Skip to Content
⚠️ This documentation is AI-generated, for personal use only, and is not supported or endorsed by Google.
GuidesVerifying a Signed Document

Verifying a Signed Document

This is the central attestation pattern: prove “I know a document D whose ECDSA signature under the public key pk is valid, and whose content satisfies predicate P,” without revealing D. Everything else on this site — field choice, witness layout, circuit compilation — composes into this one pattern. TPM2 quotes, Intel TDX reports, JWT bodies, and signed mdoc credentials all fit this shape; only the predicate P and the byte-parsing details change.

The three pieces

  • SHA-256 gadget. Hashes the document in-circuit and exposes its 256-bit digest as a wire. See SHA Gadget.
  • ECDSA gadget. Checks the signature identity in the curve’s base field. Takes (pk_x, pk_y) as public, (r, s, intermediate points) as witness, and consumes the digest from the SHA gadget. See ECDSA Gadget.
  • User predicate. Whatever property of the document you care about (attribute disclosure, range check, fresh timestamp). Composes out of the primitives in Circuits / Logic.

What the prover computes off-circuit

The prover does the heavy elliptic-curve work outside the circuit and feeds the intermediate points in as witness. Two calls carry the weight:

  • FlatSHA256Witness::transform_and_witness_message(...) — turns raw bytes into padded blocks plus the 48+64+64 intermediate round witnesses per block that the SHA gadget consumes. See circuits/sha/flatsha256_witness.h.
  • VerifyCircuit<LogicCircuit, Field, EC>::Witness — the inner struct that holds the off-circuit ECDSA witness. The prover fills it with: the point (rx, ry) on the curve, the scalar inverses rx_inv, s_inv, and pk_inv, the 8-element precomputed sum table pre[8] that holds the x- and y-coordinates of {G+pk, G+R, R+pk, G+R+pk} in the fixed order [GPK_X, GPK_Y, GR_X, GR_Y, RPK_X, RPK_Y, GRPK_X, GRPK_Y], then kBits single-field-element mux indices bi[kBits] — each selecting the table entry for that bit-step — and kBits-1 intermediate projective triples (int_x[i], int_y[i], int_z[i]) that slice the bit-loop for depth reduction. There is no bos_coster_shamir call; the circuit performs triple scalar multiplication via a precomputed 4-point sum table indexed bit-by-bit in a double-and-add loop. See circuits/ecdsa/verify_circuit.h.

Neither gadget re-derives this work in-circuit. The circuit only checks the identity the witness implies — the prover’s expensive off-circuit arithmetic is the whole point of moving to ZK.

The base-field / scalar-field split

ECDSA lives in two fields that happen to share the same curve. r and s are scalars modulo the curve order n; the coordinate arithmetic (rx, ry) = g·(e/s) + pk·(r/s) happens modulo the curve’s base prime p. p and n are the same bit-length but different numbers. The circuit handles this by:

  • Running all gates in the base field (Fp256 for secp256r1). That is what FlatSHA256Circuit and VerifyCircuit both assume.
  • Treating r and s as integers the circuit range-checks against n rather than as native scalar-field elements. The range check is a bit-by-bit comparison against a public constant (the curve order), expressed through circuits/logic/bit_adder.h.
  • Requiring the prover to witness s^{-1} in the base field and checking consistency gate-by-gate.

The consequence for callers: you do not pick the scalar field separately from the base field. Picking Fp256 fixes both.

Wiring it together

The sketch below mirrors lib/zk/zk_test.cc and lib/circuits/mdoc/mdoc_signature.h:

// 1. Instantiate the gate library. QuadCircuit<Fp256Base> Q(fp256); CompilerBackend<Fp256Base> cbk(&Q); Logic<Fp256Base, CompilerBackend<Fp256Base>> lc(&cbk, fp256); // 2. Declare public inputs: one, pk_x, pk_y, the expected digest, and // whatever else your predicate needs. auto one = lc.eltw_input(); auto pkx = lc.eltw_input(); auto pky = lc.eltw_input(); // The expected 256-bit digest is a public input declared as a v256 of bits. // (Each bit of the digest is one lc.input() call; see flatsha256_circuit_test.cc.) v256 digest_pub = declare_v256_public(lc); // ... other public inputs ... Q.private_input(); // 3. Declare witness inputs: padded message, SHA round witnesses, ECDSA // witness (rx, ry, pre[8], inverses, bi[kBits], int_{x,y,z}[kBits-1]). auto msg = declare_padded_message(lc, /* max_bytes */ 512); FlatSHA256Circuit<...>::BlockWitness sha_w[8]; for (auto& bw : sha_w) bw.input(lc); VerifyCircuit<Logic<Fp256Base, CompilerBackend<Fp256Base>>, Fp256Base, P256>::Witness ecdsa_w; ecdsa_w.input(lc); // 4. Instantiate the gadgets (they hold a reference to the Logic instance). FlatSHA256Circuit<Logic<Fp256Base, CompilerBackend<Fp256Base>>, BitPlucker</* ... */>> sha(lc); VerifyCircuit<Logic<Fp256Base, CompilerBackend<Fp256Base>>, Fp256Base, P256> ecdsa(lc, p256, n256_order); // 5. Assert SHA-256(msg) == digest_pub in-circuit. // assert_message_hash is void; digest_pub is the required target wire. sha.assert_message_hash(/* max */ 8, sha_w[0].nb /* placeholder */, msg, digest_pub, sha_w); // 6. Verify the signature against the same digest wire. ecdsa.verify_signature3(pkx, pky, lc.as_scalar(digest_pub), ecdsa_w); // 7. Apply the user predicate to the document. assert_predicate(lc, msg); // 8. Compile. auto circuit = Q.mkcircuit(/* nc */ 1);

The declare_* helpers above are not real APIs — they are placeholders for the sequence of lc.eltw_input() calls that declare each gadget’s expected witness shape. See flatsha256_circuit_test.cc and verify_circuit_test.cc for the concrete declarations. The gadget instantiation pattern (FlatSHA256Circuit sha(lc); sha.assert_message_hash(...)) follows circuits/mdoc/mdoc_hash.h, which composes both gadgets the same way.

What the user predicate looks like

assert_predicate(lc, msg) is everything that is specific to your use case. A few recurring shapes:

  • Attribute disclosure. Prove the document, parsed as a structured blob, contains a field with an expected value. See Parsing Structured Bytes.
  • Range check. Prove a numeric field (age, timestamp, measurement register) falls in a public range. circuits/logic/bit_adder.h carries the comparison primitives.
  • Freshness. Prove a timestamp field is ≥ a public now value. Same bit-adder primitives.

The predicate is the only circuit-level knob you actually own; the SHA and ECDSA pieces above are constants.

Attestation-shape examples

  • TPM2 quote. Document is a TPMS_ATTEST blob; pk is the AIK; predicate asserts the PCR digest matches an expected measurement. The SHA gadget hashes the entire TPMS_ATTEST; the ECDSA gadget verifies its signature.
  • Intel TDX TDREPORT / TDQUOTE. Document is the TDQUOTE body; pk is the PCK or quoting-enclave key; predicate asserts MRTD / RTMR fields match expected values.
  • JWT. Document is the base64url header.payload; pk is an ES256 key; predicate asserts specific claims in the payload.

In each case the pattern is unchanged — only the parsing in the predicate changes.

Last updated on