Skip to content

feat(circuits)!: per-platform circuits output id and handle nodes - #17

Open
SupremaLex wants to merge 37 commits into
mainfrom
feat/hashed-identities
Open

SupremaLex wants to merge 37 commits into
mainfrom
feat/hashed-identities

Conversation

@SupremaLex

@SupremaLex SupremaLex commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Part of the hashed-identities change: a claim puts no plaintext user id, handle or email on chain. The circuits open the committed id and handle, check them against the platform's rules, and output tagged SHA-256 nodes. Two per-platform bearer-link circuits replace the shared bearer-link, and oidc-google outputs nodes in place of the sub digest and the packed email. Companion PRs: libid-org/libID-rs#35 and libid-org/libID-contracts#87.

What changes

bearer-link-x and bearer-link-github

Each circuit:

  • opens the two bearer commitments (token response, identity request) over the same bearer;
  • opens the raw id and raw handle commitments;
  • checks both against the platform's rules, which refuse and never trim;
  • folds the handle from the same witness that opened its commitment;
  • outputs idNode = SHA256("libid.<p>.user-id" || id) and handleNode = SHA256("libid.<p>.handle" || fold(handle)).

The rules are compiled into each circuit, so a proof cannot cross platforms. The relation is written once, as identity_link in lib/identity; each main selects its platform's constants.

Public inputs, 72 each:

Index Value
0-31 token commitment, one byte per field
32-63 identity commitment, one byte per field
64-65 id commitment, 16-byte halves
66-67 handle commitment
68-69 idNode
70-71 handleNode

oidc-google

  • Checks the email against the Google rules and folds it. The rules admit no " or \, so the email the circuit reads cannot run past its own JSON string into another member.
  • The id node takes the tag libid.google.user-id. The sub refuses " and \.
  • Public inputs, 57: 0-31 Authorization Digest, 32-33 SHA256(aud), 34-35 idNode, 36-37 handleNode, 38 exp, 39-56 modulus (18 limbs).

lib/identity

Commitment opening, the tagged hash, node_halves, and the handle and id rules. A testing module holds the test witness builders; no main calls it.

Generated table

lib/identity/src/table.nr holds the platform tags and rules. table_tests.nr holds one test per handles.json vector. Both are generated from libID-contracts' solidity/contracts/handles/handles.json. scripts/build.sh ships the table with every circuit as handles-table.nr.

contracts.ref

Pins the libID-contracts commit the table must match. A new CI job, Handle table, fetches that commit and runs its regen-identity-handles.py --compare-noir and --compare-noir-tests. It also checks the vendored identity-link fixtures and the Google fixture's digest against libID-contracts' session fixtures.

Witnesses

  • scripts/identity-link-witness.py writes a bearer-link Prover.toml from a fixture marked "provenance": {"synthetic": true}, and refuses anything else.
  • scripts/google-fixture-witness.py writes the Google witness from a fixed test key, with a mixed-case email.
  • CI regenerates all three Prover.toml and fails on a difference.

Spec

oidc-google's public inputs do not match REQ-PLAT-16B's list: the id and handle nodes stand where its userId digest and raw email are. The spec needs the matching update.

Boundaries

  • The circuit owns raw → node: the rules, the fold and the tagged hash run here, over the witness that opens the commitments.
  • libID-contracts' verifiers own the framing and the public-input layout. They read the nodes and never the raw values.
  • handles.json in libID-contracts is the one source of rules and tags. This repo holds a generated copy, checked against it in CI.

Measured

Gates from bb gates; prove time is native bb prove; verify gas is libID-contracts' forge gas snapshot, verify as a call.

Circuit Gates Size Prove Verify gas vk_hash
bearer-link-x 77,505 2^17 0.70 s 740,178 0x1f09866b4c8feca602a2d394a5c8c924d3b8d964696f0214252ab97c9213775d
bearer-link-github 79,445 2^17 0.75 s 740,178 0x1168344f46c63b1fa251c174f4b4d4e7c104cf65ab38d36fcbcf66108e626770
oidc-google 199,738 2^18 1.52 s 755,087 0x2b2c5f9b3301f9ba7b6d69db7baaebc124b09959b7fb7ae87734ecb56667a488

Consumers that must move

These circuits replace v0.6.0's ABIs. bearer_link (two public inputs) becomes bearer_link_x and bearer_link_github. oidc_google's user_id_hash and email_packed become id_node and handle_node.

  • libID's browser prover (ts/packages/ceremony) must commit the identity response's id and handle as their own ranges, as libid-rs does; pass their openings and nodes to the platform's bearer-link circuit; take oidc_google's node inputs; and pin the first release that ships these circuits.
  • libID-contracts deploys the verifiers of that same release. Its handles.json at contracts.ref is the table they are built from.

None of these circuits is released yet.

Privacy

The circuits never put the id, handle, sub or address on chain in plaintext. That is not secrecy: a node is an unsalted tagged SHA-256, so anyone can hash a guess and compare. In the bearer-link flows the byte lengths are public. GitHub ids are sequential, so a GitHub id node is in effect public.

Breaking changes

  • bearer-link and BearerLinkHonkVerifier are removed. bearer-link-x / BearerLinkXHonkVerifier and bearer-link-github / BearerLinkGithubHonkVerifier replace them.
  • oidc-google's public inputs 34-37 are the id and handle nodes; its private inputs change to match.
  • Both bearer-link circuits are 2^17. A prover sized for a 2^16 circuit needs the larger SRS.
  • Every vk changes, so every deployed verifier changes.

Verification

Ran

  • CI at 04cf620 is green: Circuits, Handle table, DCO. Circuits runs nargo fmt --check, shellcheck, nargo test (lib/identity 82, bearer_link_x 8, bearer_link_github 5, oidc_google 12), witness regeneration, a full build and check-verifiers.sh. Each verifier accepts its witness's proof on anvil and rejects it with one bit flipped.
  • Locally, scripts/build.sh under the pinned toolchain gives the gate counts and vk hashes above.

Synthetic

  • Every witness. X and GitHub come from libid-rs' ceremony_fixtures (handles Alice_1, OctoCat); Google from a fixed test key.

Not run

  • No proof over a real capture. No browser proving; the times above are native.
  • check-verifiers.sh fails if port 8545 is in use, because its anvil binds HTTP beside the IPC socket.

Before merge

  • contracts.ref (87f7a0d) is a commit on libID-contracts' feat/hashed-identities branch. It must move to the merged commit on libID-contracts' main; the Handle table job fails once the branch is deleted.

Open questions

  • Google address rules. After folding, only [a-z0-9.+-_@], at most 62 bytes. Workspace addresses with an apostrophe or more than 62 bytes have no handle node and cannot bind. Widening later moves no existing node, but needs a new vk and verifier, and a longer cap costs gates. Accept this for the first release?

Stack

#18 is stacked on this PR. It changes the bearer-link layout to 12 public inputs, caps the GitHub bearer at 47 bytes, and changes all three vks. The 72-input layout above does not ship on its own.

A new circuit for X: the bearer link, plus the id and handle the identity
response now commits instead of revealing. It opens both values as X sent
them, checks them against X's rules, folds the handle byte by byte from the
same witness that opened its commitment, and outputs

  idNode     = SHA256("libid.x.user-id" || id)
  handleNode = SHA256("libid.x.handle"  || fold(handle))

72 public inputs: the two bearer commitments as bytes, then the id and
handle commitments and the two nodes as 16-byte big-endian halves.

lib/identity holds what the identity circuits share: commitment openings,
the tagged hash, halves, and the handle and id rules, which refuse rather
than trim. Its constants and one test per vector (src/table.nr) are
generated from libid-contracts' handles.json, the table Solidity, Rust and
TypeScript run; build.sh writes that table's SHA-256 beside the artifacts.
scripts/identity-link-witness.py turns the witness libid-rs emits into a
Prover.toml; the committed one is the libid-rs X fixture (handle Alice_1).

77,505 gates (bearer-link: 41,799). The verifier accepts the proof on anvil
for 916,542 gas and rejects a flipped bit.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
GitHub gets the relation bearer-link-x states for X: the bearer link, plus
the committed id (a JSON integer's digits, no leading zero) and login
opened as GitHub sent them, the login folded under GitHub's rules, and

  idNode     = SHA256("libid.github.user-id" || id)
  handleNode = SHA256("libid.github.handle"  || fold(login))

out through the same 72 public inputs. 79,445 gates; the verifier accepts
on anvil for 916,422 gas and rejects a flipped bit.

The shared bearer-link circuit goes: no platform proves under it any more.

scripts/identity-link-witness.py reads table.nr constants whole, since
nargo fmt wraps the long ones, and build.sh does the same for the table
digest. The committed Prover.toml is libid-rs' GitHub fixture (OctoCat).

BREAKING CHANGE: bearer-link and BearerLinkHonkVerifier are removed; X
proves under bearer-link-x and GitHub under bearer-link-github.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
oidc-google checks the signed email against the Google rules (charset,
one `@` not at an edge, at most 62 bytes, zero tail), folds it, and
exposes handleNode = SHA256("libid.google.handle" || folded address) at
public inputs 36-37 in place of the packed email bytes. The count stays 57,
so exp and the modulus keep their offsets.

The id node takes the uniform tag: SHA256("libid.google.user-id" || sub).
The sub's byte rules are lib/identity's check_id under the Google id rules,
which also refuses a backslash. Both hashes are lib/identity's tagged_hash.

scripts/google-fixture-witness.py writes the committed witness from a
seeded RSA-2048 key, so the token is reproducible; its email is
Fixture@Example.com, mixed case, so the fold is exercised. 199,738 gates
(was 182,952); the verifier accepts on anvil for 937,931 gas and rejects a
flipped bit.

The X and GitHub circuits' header comments now say where public inputs
68-71 come from: the payload's nodes, which the proof binds.

BREAKING CHANGE: public inputs 34-37 are the id and handle nodes; the
email is no longer public and the user id tag is libid.google.user-id.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The table's SHA-256 was written at the artifacts root, which release.yml
never packages, so the table identity did not ship. build.sh now copies
lib/identity/src/table.nr into every circuit that depends on
lib/identity as handles-table.nr, with handles.json.sha256 beside it.
release.yml tars and hashes each circuit directory whole, so both files
reach the tarball and manifest.json. The root file is gone; nothing read
it.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
A `*.secret.json` identity-link witness holds a live bearer, and so does
the Prover.toml written from it. identity-link-witness.py now refuses to
write such a witness to stdout, refuses an --out path git tracks (checked
before the file is opened), and writes --out with mode 0600, set before
any byte. A commitment that does not open names the witness entry that
failed; no message prints a value.

MAX_BEARER_LEN is read from lib/identity/src/lib.nr and the tags and
buffer sizes from table.nr through scripts/identity_table.py, so the
script restates none of them.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
google-fixture-witness.py reads the Google tags and the address and sub
buffer sizes from lib/identity's generated table instead of restating
them, and names where DIGEST comes from. CI regenerates the witness into
a temp file and diffs it against the committed Prover.toml; the key is
seeded, so the output is deterministic.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
bearer-link-x and bearer-link-github repeated the same relation. It now
lives in lib/identity as identity_link, generic over the tag and buffer
sizes; each main passes its inputs through in order with its platform's
tags and rules. Public inputs, gate counts (77505, 79445) and vk_hash are
unchanged.

The two bearer commitments fail with their own messages, "token bearer
commitment mismatch" and "identity bearer commitment mismatch", in place
of the shared "hash commit mismatch"; verify_hash_commit, whose only
callers were these, is gone. bearer-link-x gains a test for the token
label.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
constrain_bearer and hash_commit moved into lib/identity without the
tests the retired bearer-link circuit carried. They return in
src/tests.nr: an empty bearer, CR, LF, DEL and a byte above 0x7e
refused, a dirty tail and a length past the buffer refused, the
shortest (one 0x20) and longest (128 x 0x7e) bearers accepted, and
hashlib vectors for hash_commit at 4 bytes and at both bearer extremes.

table.nr states each platform's lengths twice, as buffer sizes and in
the rules; a test asserts MAX_HANDLE_* == HANDLE_RULES_*.max_len and
MAX_ID_* == ID_RULES_*.max_len for X, GitHub and Google.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The sub-byte tests left with assert_sub_byte; they return against
check_id under ID_RULES_GOOGLE: 0x1f, 0x7f, 0x80, `"` and `\` refused,
and every other byte of 0x20..0x7e accepted.

EMAIL_MAX and SUB_MAX are MAX_HANDLE_GOOGLE and MAX_ID_GOOGLE, and the
tag lengths are the generated tags' len(), so the circuit restates none
of the table. Gate count (199738) and vk_hash are unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
gen-verifier.sh's usage and the README's verifier list name all three
circuits; the key-history paragraphs state how the current keys relate
to v0.5.0's. The README and lib/identity cite libid-contracts' handle
table at its new path, solidity/contracts/handles/handles.json, and the
README notes identity_link and the secret-witness rules of
identity-link-witness.py.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…ened

A symlink or a missing directory raised a bare OSError traceback; the
script now exits with the reason and what to pass instead.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…racts

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…epo-qualified banner)

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…ain.nr

google-fixture-witness.py restated SIGNING_INPUT_MAX, PAYLOAD_JSON_MAX,
AUDIENCE_MAX, NUM_LIMBS and MOD_BITS. identity_table.py now reads a plain
`global` as well as a `pub global`, from any given source, and the
script takes the five from circuits/oidc-google/src/main.nr. The
regenerated witness is byte-identical.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…he synthetic ones

identity-link-witness.py treated a witness as secret only when its file
was named *.secret.json. It now reads the form: a session file with an
`identity_link_witness` member is libid-rs' synthetic fixture; a bare
witness (`platform`, `token_bearer`, ... at the top level) is a real
capture and secret whatever its name.

For a secret witness --out must end in .toml and lie outside every git
work tree, or be matched by that work tree's ignore rules
(`git check-ignore`, which never matches a tracked file); stdout is
refused. After writing it the script prints, on stderr, how to prove
it from outside the repo: umask 077 and
`nargo execute -p /abs/stem /abs/stem`, which reads stem.toml and
writes the solved witness to stem.gz instead of target/<pkg>.gz.

The README documents that and that the id and handle blinders link the
signed record to the account for as long as it exists, not only until
the bearer is revoked.

The two synthetic witnesses are vendored, wrapped as session files, in
fixtures/{x,github}-identity-link-witness.json from libID-contracts'
ceremony session fixtures. The generated Prover.toml now names its
source in a header comment; both committed ones are regenerated (header
only). CI regenerates them and diffs, and checks the guard: a bare
witness to stdout or to an unignored path in the work tree exits
non-zero, and one outside it is written 0600 with nothing on stdout.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
build.sh wrote a per-circuit handles.json.sha256 that copied
table.nr's TABLE_SHA256 label rather than hashing anything, and nothing
read it: libID-contracts vendors a release by comparing handles-table.nr
itself (regen-identity-handles.py --compare-noir). The file is gone from
the build, the release comment and the README; handles-table.nr stays.
No release has shipped it.

contracts.ref pins the libID-contracts commit whose handles.json the
table must match. A new handles-table job checks that commit out and
runs its regen-identity-handles.py --compare-noir against
lib/identity/src/table.nr; it needs only Python. The README says how to
move the pin.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…er links

lib/identity gains a testing module with blinder() and padded(), which
the two bearer-link circuits' tests now import instead of each keeping
its own. Both tests' Case carries bearer_len, id_len and handle_len, and
run() passes them through, so the two suites have the same structure.
No main calls the module, so no circuit's constraints change.

lib/identity also tests that check_id refuses a nonzero byte past the
id ("id tail must be zero-padded").

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…ails

The circuit's address check is normalize_handle under
HANDLE_RULES_GOOGLE: the fixture address folds, an address holding `"`
is refused, and a nonzero byte past the address or past the `sub` is
refused. The table's refused Google vectors already cover `"` and `\`
in a handle and in a `sub` (handle_google_46/47, id_google_19/20).
padded_sub gives way to lib/identity's padded.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The vk section's account of how the keys differ from v0.5.0's, and the
note that the sources were once extracted from the monorepo and
formatted, are history; the vk table and the fmt check stay. The
witness section says how nargo writes the solved witness under the
umask.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
identity-link-witness.py now gives its "neither a session file nor a
bare witness" error for a JSON array or scalar too. The README and
google-fixture-witness.py spell the repository libID-contracts.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…fail closed on where it goes

identity-link-witness.py decides a witness's secrecy and destination
this way:

- A session file is synthetic only when its `provenance` is an object
  with `"synthetic": true`; the two vendored fixtures carry it. A bare
  witness and a session file without the marker are secret.
- A secret --out counts as inside a git work tree unless git states
  "not a git repository" for its directory and no parent directory
  holds a `.git` entry. A repository git cannot read (dubious ownership,
  a broken .git) is refused unless check-ignore matches the path, which
  it cannot do there. Git runs under LC_ALL=C so its message is matched
  untranslated.
- An --out stem containing `.` is refused: nargo's `-p x.secret` reads
  x.toml.
- Only `x` and `github` are accepted, before any table constant is read;
  identity_table.constant escapes the name it searches for.
- The printed instructions quote every path, scope `umask 077` to each
  command's subshell, read the package name from the circuit's
  Nargo.toml, and add the `bb prove` step for the solved witness.

The CI guard checks an unmarked session file and dubious ownership
(GIT_TEST_ASSUME_DIFFERENT_OWNER=1) as well. The README's example uses
`mktemp -d`, which works on macOS too.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…table.py

Both witness scripts encoded a 32-byte value as the circuits' [high,
low] halves and zero-padded a value into its buffer, each with its own
copy. identity_table.py now holds one of each: `halves` returns the two
integers and each script keeps its own text form (decimal for the
bearer links, 0x hex for oidc-google), and `padded` refuses a value
longer than its buffer with SystemExit, naming the input. All three
Prover.toml files regenerate byte-identical.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…he merge order when the pin is gone

At the libID-contracts commit contracts.ref pins, the job now also
checks that fixtures/{x,github}-identity-link-witness.json hold the
`identity_link_witness` members of the ceremony session fixtures (the
provenance marker aside), and that google-fixture-witness.py's DIGEST is
their `authorization_digest`.

The pin is fetched with plain git instead of actions/checkout, so a
commit libID-contracts no longer serves fails with the fix: merge the
libID-contracts change first, then point contracts.ref at its commit on
main. The README states that order.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…testing

`blinder` and `padded` were `pub mod testing` in libid_identity, the
library every circuit's main calls, so a main could import them. They
now live in their own package, libid_identity_testing, with no
dependencies; `blinder` takes its width from the call site, so
BLINDER_LEN is not restated. lib/identity's tests use `blinder(0)` in
place of their own counting_blinder, which built the same bytes.

Nargo has no test-only dependencies, so the circuits and lib/identity
declare the package as an ordinary dependency and import it in their
test sections only. lib/identity therefore lists a test package among
its dependencies; it is not re-exported, and a package reaches it only
by declaring it. The alternative, a private copy of `padded` in
lib/identity's tests, would keep a second copy of the builder.

All three vk_hash values are unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…-google

oidc-google hashed its id and handle nodes with its own user_id_digest
and handle_digest wrappers over tagged_hash, then split them into
halves by hand. lib/identity now has node_halves(tag, value, len), the
node as the public inputs carry it, and both identity_link and
oidc-google call it where they computed the node before. The id
digest tests check node_halves under the Google tag against the same
hashlib vectors, written as halves.

The helper does not run check_id or normalize_handle: the callers keep
those where they were. A helper that runs the check too moves either
the check or the node hash relative to the commitment and claim
asserts, and that reorders the circuit: bearer-link-x's vk_hash changed
with the check-inclusive helper called at the check site and called
after the commitments. With node_halves all three vk_hash values are
unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The README lists what pins v0.6.0's ABIs and must move: libID's browser
prover (ts/packages/ceremony on libID's main, pinned to v0.6.0, which
proves the shared bearer_link circuit and v0.6.0's oidc_google ABI) and
libID-contracts' verifiers, which move to the same release.

oidc-google's public-input comment no longer claims REQ-PLAT-16B's list
exactly: the id and handle nodes stand in place of its `userId` digest
and raw `email`.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…eption

A safe.directory entry in the runner's git config makes git skip the
ownership check, and the guard's dubious-ownership case then passes
through the ordinary in-work-tree refusal, which the fail-open script
passed too. The case now runs with GIT_CONFIG_GLOBAL and
GIT_CONFIG_SYSTEM at /dev/null, and the step fails first unless git
reports dubious ownership. Against the previous script the step fails
with "a bare witness was written into a work tree git refused to read".

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…y::testing

`blinder` and `padded` live in a `testing` module of libid_identity
instead of a separate lib/identity-testing package, so no Nargo.toml
declares a second path dependency for tests alone. No circuit's main
calls them; the three vk_hash values are unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
oidc-google drops the `sub` and address tests the generated table
already runs (`"`, `\`, a tab, the fold, a quote in the address) and
keeps DEL, a non-ASCII byte and the printable sweep. The zero-tail case
for a handle runs once in lib/identity's tests instead of in each
circuit. bearer-link-github keeps the underscore test, which shows
HANDLE_RULES_GITHUB is the selected rule set, and drops the doubled
hyphen the table covers. tests.nr keeps one control-byte bearer test
(CR, REQ-COMMON-37) and one above-range test (DEL).

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…o tails buy

identity_link's doc in lib.nr is the one statement of the relation;
the bearer-link mains name the platform's endpoints, the public-input
layout, and point to it. lib.nr's header no longer repeats the node
formulas.

sha256_var hashes only the first `len` bytes, so the zero-tail checks in
check_id, normalize_handle and oidc-google's address and `sub` do not
make a node or commitment unique; they make the padded witness
canonical. The comments say so.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
identity-link-witness.py: a secret witness (anything not marked
synthetic) requires --out, is written with mode 0600 through an
O_NOFOLLOW open and fchmod before any byte, and never goes to stdout.
The git work-tree probing, the dubious-ownership case, the nargo
file-name rule and the printed proving commands are gone; the README
section and the CI guard say what remains: write it outside the repo,
delete it after proving. The CI guard checks one bare witness: refused
on stdout, 0600 under --out.

google-fixture-witness.py holds the fixture key as constants N, E, D,
the key the seeded generator produced, instead of regenerating it with a
SHA-256 stream and Miller-Rabin; it checks that D inverts E mod N. The
DIGEST provenance is stated once.

identity_table.toml_array is the one Prover.toml array writer; the
bearer-link script quotes every value, the Google script only strings,
as before. All three committed Prover.toml regenerate byte-identical.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
Every circuit takes its rules and tags from lib/identity's table, so
build.sh copies it unconditionally instead of grepping each Nargo.toml
for a libid_identity dependency.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
… order

The consumers that move with these circuits belong in the PR and the
release notes, not in the README. The contracts.ref merge order is four
lines; the drift-check description is unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
table.nr now holds only the platform tags and rules the circuits compile
against, and what build.sh ships as handles-table.nr (55 lines, from
1011). The vector tests are generated into table_tests.nr beside it,
assert through node_halves against two Field literals, and build their
witnesses with testing::padded from string literals.

contracts.ref moves to the libID-contracts commit whose generator writes
both files. CI compares table.nr with --compare-noir and table_tests.nr
with --compare-noir-tests.

The bytecode, the vks and the Solidity verifiers are byte-identical
(vk_hash bearer-link-x 0x1f09866b..., bearer-link-github 0x1168344f...,
oidc-google 0x2b2c5f9b...); the ACIR json's source hash moves with lib.nr.
lib/identity still runs 82 tests.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
libid-rs' capture_ceremony writes no private witness, so nothing
produces a secret witness for this script to handle. It now accepts
only a session file marked "provenance": {"synthetic": true} and
refuses anything else with a one-line error; output goes to stdout or
--out as plain text.

Drop the secret-witness path with it: the 0600/O_NOFOLLOW writer, the
secret header and stdout refusal, the CI "Secret witness guard" step
and the README "Proving a real capture" section.

The committed bearer-link Prover.toml files regenerate byte-identical.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
Each comment states what the code does, with at most one line of reason.
The design rationale stays in the PR description.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
@SupremaLex
SupremaLex marked this pull request as ready for review October 9, 2026 14:56
@SupremaLex
SupremaLex requested a review from xgreenx October 9, 2026 14:56
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