Skip to content

Conversation

@0x471
Copy link

@0x471 0x471 commented Oct 24, 2025

Description

This PR implements a circuit for verifying Google Cloud Platform Confidential Space JWT attestations with X.509 certificate chain validation and TEE-specific claim extraction (EAT nonce and Docker image hash).

Changes

  • Updated Powers of Tau download source URL since the previous one was down (e16daba)

  • Fixed circom compilation by adding correct node_modules library paths for @zk-kit and circomlib dependencies (0f963573)

  • Added JWT verification circuit dependencies: @zk-email/jwt-tx-builder-circuits, @zk-email/jwt-tx-builder-helpers, node-forge (b532bd98)

  • Created extractAndValidatePubkey circuit to extract and validate X.509 certificate public keys with ASN.1 structure verification (eb497f62)

  • Created verifyCertificateSignature circuit to hash TBS certificates and verify RSA signatures (8356718b)

  • Created verifyExtractedString circuit for generic string extraction with padding validation, used by EAT nonce and image digest extraction (2fd97583)

  • Implemented GCP JWT verifier circuit with complete 3-signature certificate chain validation, EAT nonce
    extraction, and container image digest verification as public outputs (13ae2194)

  • Added build script for GCP JWT verifier that compiles circuit, generates proving/verification keys, and
    exports Solidity verifier (ad0c626a)

Summary by CodeRabbit

  • New Features

    • Added a GCP JWT verifier with full X.509 chain checks, EAT nonce and container image_digest extraction/validation.
  • Documentation

    • Added comprehensive README describing architecture, public outputs, usage, build and verification steps, and references.
  • Chores

    • Added example JWT fixture, build scripts and npm script; updated build helper paths and ptau download source; added JWT tooling helpers and circuit builder dependencies.

Note

Adds a GCP JWT verifier circuit that validates the full x5c certificate chain and extracts EAT nonce and container image hash, with input prep tooling, build script, and dependencies.

  • Circuits:
    • circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom: New main circuit verifying JWT (RSA) and full x5c[0..2] chain; outputs root CA pubkey hash, eat_nonce[0] (b64url), and image SHA256 hash.
    • Support components: jwt_verifier.circom, utils/gcp_jwt/extractAndValidatePubkey.circom, utils/gcp_jwt/verifyCertificateSignature.circom, utils/gcp_jwt/verifyJSONFieldExtraction.circom.
  • Tooling & Docs:
    • Input prep: prepare.ts to parse JWT, extract x5c certs/keys/signatures, locate eat_nonce and image_digest, and emit circuit inputs; adds example_jwt.txt.
    • Build: scripts/build/build_gcp_jwt_verifier.sh and npm script build-gcp-jwt-verifier to compile and generate keys.
    • Dependencies: Add @zk-email/jwt-tx-builder-circuits, @zk-email/jwt-tx-builder-helpers, and node-forge in package.json.
    • README: Usage, architecture, and proof generation for the GCP JWT verifier.

Written by Cursor Bugbot for commit fcb40e9. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new GCP JWT Verifier circuit and supporting tooling: Circom templates for JWT parsing and full x5c certificate-chain verification, certificate/public-key utilities, JSON-field extraction, a TypeScript input-preparation script, README and example fixture, plus build script and build-system path/ptau updates.

Changes

Cohort / File(s) Summary
Documentation & Example
circuits/circuits/gcp_jwt_verifier/README.md, circuits/circuits/gcp_jwt_verifier/example_jwt.txt
New README describing circuit purpose, architecture, usage; example JWT fixture added.
Core Circuit
circuits/circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom
New GCPJWTVerifier(signatureAlgorithm, n, k) template (instantiated as main) enforcing 3-cert x5c chain verification, JWT signature check, EAT nonce and image_digest extraction, and public outputs (root pubkey hash, nonce, image hash).
JWT Helpers
circuits/circuits/gcp_jwt_verifier/jwt_verifier.circom
New JWT helper templates: SelectSubArrayBase64, FindRealMessageLength, CountCharOccurrences, and JWTVerifier integrating SHA-256 and RSA-SHA256 verification.
Pubkey & Cert Utilities
circuits/circuits/utils/gcp_jwt/extractAndValidatePubkey.circom, circuits/circuits/utils/gcp_jwt/verifyCertificateSignature.circom, circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom
New templates to extract/validate ASN.1 pubkey regions, dynamic-length cert hashing + signature verification, and JSON key/value extraction with positional and structural checks.
Input Prep Script
circuits/circuits/gcp_jwt_verifier/prepare.ts
New TypeScript script that parses JWT and x5c chain, derives DER/padded bytes, chunks pubkeys/signatures to circuit format, finds payload offsets for eat_nonce/image_digest, validates formats/lengths, and emits circuit_inputs.json.
Build / Packaging
circuits/package.json, circuits/scripts/build/build_gcp_jwt_verifier.sh, circuits/scripts/build/common.sh
Adds npm script and dependencies, new build shell for the verifier, updates ptau download URL to GCS, and changes circom -l linker include paths to reference node_modules directly.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant CLI as prepare.ts (CLI)
    participant JWT as JWT File (header.payload.sig + x5c[])
    participant Circuit as GCPJWTVerifier Circuit
    participant CertUtils as Cert Utilities
    participant SigVer as SignatureVerifier
    Note over CLI,JWT: prepare.ts parses JWT + x5c chain and builds circuit inputs
    CLI->>JWT: read JWT + x5c[0..2]
    CLI->>CertUtils: parse certs -> DER, pubkeys, offsets, signatures
    CLI->>Circuit: write circuit_inputs.json
    Note over Circuit,CertUtils: in-circuit verification flow
    Circuit->>SigVer: SHA-256(message) then RSA verify (leaf pubkey)
    Circuit->>CertUtils: ExtractAndValidatePubkey(x5c[0]) -> leaf pubkey
    Circuit->>SigVer: VerifyCertificateSignature(x5c[0] signed by x5c[1])
    Circuit->>SigVer: VerifyCertificateSignature(x5c[1] signed by x5c[2])
    Circuit->>CertUtils: ExtractAndValidatePubkey(x5c[2]) -> root pubkey -> compute hash
    Circuit->>Circuit: ExtractAndVerifyJSONField(payload, eat_nonce)
    Circuit->>Circuit: ExtractAndVerifyJSONField(payload, image_digest)
    Circuit-->>CLI: public outputs (publicKeyHash, eat_nonce, image_hash)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Focus review on cryptographic parameter consistency (n, k, 120-bit chunking, kLengthFactor) across templates.
  • Validate prepare.ts handling of offsets, padding, encoding and exact byte positions to match in-circuit expectations.
  • Inspect JSON-field extraction for edge cases (whitespace variants, escaped quotes) that could cause mismatches or false positives.

Possibly related issues

  • #1291 — Matches the ptau download URL and circom linker include-path changes; this PR implements those build-system updates.

Possibly related PRs

  • #1342 — Edits the same build helper script (ptau URL and linker-path adjustments); likely related to build changes here.
  • #986 — Adds/updates RSA signature-verification components and signatureAlgorithm handling that the new circuits rely on.
  • #656 — Similar build-script/linker changes; potentially overlapping with the common.sh edits.

Suggested labels

staging

Suggested reviewers

  • remicolin
  • motemotech

Poem

🔐 Three certificates climb from leaf up to root,
Circuits check signatures, each byte absolute.
Nonce and digest revealed, offsets aligned,
A proof-ready verifier, deterministic and signed.
⚡️

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding a GCP JWT verifier circuit with TEE attestation claims extraction, which matches the comprehensive implementation across multiple circuit files, tooling, and documentation in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb2c5dd and ad0c626.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • circuits/circuits/gcp_jwt_verifier/README.md (1 hunks)
  • circuits/circuits/gcp_jwt_verifier/example_jwt.txt (1 hunks)
  • circuits/circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom (1 hunks)
  • circuits/circuits/gcp_jwt_verifier/jwt_verifier.circom (1 hunks)
  • circuits/circuits/gcp_jwt_verifier/prepare.ts (1 hunks)
  • circuits/circuits/utils/gcp_jwt/extractAndValidatePubkey.circom (1 hunks)
  • circuits/circuits/utils/gcp_jwt/verifyCertificateSignature.circom (1 hunks)
  • circuits/circuits/utils/gcp_jwt/verifyExtractedString.circom (1 hunks)
  • circuits/package.json (2 hunks)
  • circuits/scripts/build/build_gcp_jwt_verifier.sh (1 hunks)
  • circuits/scripts/build/common.sh (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use Yarn only for package management (yarn install/add/remove); do not use npm or pnpm in scripts

Files:

  • circuits/package.json
circuits/**/*.circom

⚙️ CodeRabbit configuration file

circuits/**/*.circom: Review ZK circuit code for:

  • Circuit correctness and completeness
  • Constraint efficiency
  • Input validation
  • Security considerations for zero-knowledge proofs

Files:

  • circuits/circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom
  • circuits/circuits/utils/gcp_jwt/verifyExtractedString.circom
  • circuits/circuits/utils/gcp_jwt/extractAndValidatePubkey.circom
  • circuits/circuits/gcp_jwt_verifier/jwt_verifier.circom
  • circuits/circuits/utils/gcp_jwt/verifyCertificateSignature.circom
**/*.{js,ts,tsx,jsx,sol,nr}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,ts,tsx,jsx,sol,nr}: NEVER log sensitive data including PII (names, DOB, passport numbers, addresses), credentials, tokens, API keys, private keys, or session identifiers.
ALWAYS redact/mask sensitive fields in logs using consistent patterns (e.g., ***-***-1234 for passport numbers, J*** D*** for names).

Files:

  • circuits/circuits/gcp_jwt_verifier/prepare.ts
🧬 Code graph analysis (1)
circuits/scripts/build/build_gcp_jwt_verifier.sh (1)
circuits/scripts/build/common.sh (2)
  • download_ptau (28-40)
  • build_circuit (50-153)
🔇 Additional comments (3)
circuits/package.json (1)

11-11: Yarn-only usage looks good; script entry is fine.
No issues spotted with the new script from a package manager policy standpoint.

circuits/scripts/build/common.sh (1)

75-79: Ensure all circuit include roots are covered.

If any includes resolve like "@zk-email/..." you may also need a linker path for that package’s circuits directory, or rely on "-l node_modules" resolving module roots correctly. Verify in CI to avoid linker surprises.

circuits/circuits/gcp_jwt_verifier/prepare.ts (1)

47-56: Repository unavailable for verification—manual review required.

The codebase could not be accessed to verify the concerns about the hex substring offset detection logic (lines 47-56). While the review identifies a potentially legitimate risk—that indexOf() searches on hex-encoded DER data can produce incorrect offsets due to encoding variations—I cannot confirm:

  • Whether the current implementation actually causes failures or is production-tested
  • If ASN.1 parsing libraries are already available as dependencies
  • How the offset is used downstream in circuit validation
  • The actual severity relative to other code risks

Action required: Manually verify that this offset calculation is resilient across all expected JWT certificate encodings from GCP, or implement proper ASN.1 parsing with verified SubjectPublicKeyInfo extraction.

Comment on lines +121 to +143
// Verify x5c[0] signature using x5c[1] public key
VerifyCertificateSignature(signatureAlgorithm, n, k, MAX_CERT_LENGTH)(
leaf_cert,
leaf_cert_padded_length,
intermediate_pubkey,
leaf_signature
);

// Extract and validate x5c[2] public key
ExtractAndValidatePubkey(signatureAlgorithm, n, k, MAX_CERT_LENGTH, MAX_PUBKEY_PREFIX, MAX_PUBKEY_LENGTH)(
root_cert,
root_pubkey_offset,
root_pubkey_actual_size,
root_pubkey
);

// Verify x5c[1] signature using x5c[2] public key
VerifyCertificateSignature(signatureAlgorithm, n, k, MAX_CERT_LENGTH)(
intermediate_cert,
intermediate_cert_padded_length,
root_pubkey,
intermediate_signature
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

No root‑of‑trust anchor; chain is unanchored and therefore forgeable.

You verify leaf←intermediate←root signatures but never pin the root to a trusted key. A prover can pick any self‑consistent chain. Anchor root (e.g., SPKI hash) as a constant or public input checked in‑circuit, and bind x5c[] used here to the JWT header’s x5c.

  • Add a public input expected_root_spki_hash (or hardcode for GCP CA) and constrain Poseidon(root_pubkey) to match.
  • Optionally also output header hash and assert header.x5c[0..2] digests correspond to the provided certificates to prevent chain substitution.
🤖 Prompt for AI Agents
In circuits/circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom around lines
121-143 the chain is unanchored (you verify leaf←intermediate←root but never pin
the root), so add a root-of-trust check: introduce a public input (e.g.
expected_root_spki_hash) or hardcode the known GCP CA SPKI hash, compute the
Poseidon hash (or existing digest primitive used elsewhere) over root_pubkey (or
its SPKI encoding) and constrain that hash to equal expected_root_spki_hash;
additionally, bind the x5c entries to the JWT header by hashing the
header.x5c[0..2] (or header hash) and asserting those digests correspond to the
certificate blobs used above to prevent chain substitution.

Comment on lines 145 to 175
// Extract substring from payload at the claimed offset
signal extracted_eat_nonce[MAX_EAT_NONCE_B64_LENGTH] <== SelectSubArray(
maxPayloadLength,
MAX_EAT_NONCE_B64_LENGTH
)(
payload,
eat_nonce_0_offset,
eat_nonce_0_b64_length
);

// GCP spec: nonce must be 10-74 bytes decoded
// Base64url encoding: 10 bytes = 14 chars, 74 bytes = 99 chars
// https://cloud.google.com/confidential-computing/confidential-space/docs/connect-external-resources

// Make sure nonce is not empty
component length_nonzero = IsZero();
length_nonzero.in <== eat_nonce_0_b64_length;
length_nonzero.out === 0; // Must NOT be zero

// Validate nonce minimum length (10 bytes decoded = 14 base64url chars)
component length_min_check = GreaterEqThan(log2Ceil(MAX_EAT_NONCE_B64_LENGTH));
length_min_check.in[0] <== eat_nonce_0_b64_length;
length_min_check.in[1] <== 14;
length_min_check.out === 1;

// Validate nonce maximum length (74 bytes decoded = 99 base64url chars)
component length_max_check = LessEqThan(log2Ceil(MAX_EAT_NONCE_B64_LENGTH));
length_max_check.in[0] <== eat_nonce_0_b64_length;
length_max_check.in[1] <== 99;
length_max_check.out === 1;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nonce length logic inconsistent with spec and MAX length; add charset check and structural binding.

  • MAX_EAT_NONCE_B64_LENGTH is 88 (64 bytes), but you check ≤ 99 chars (74 bytes). Decide one spec and enforce it.
  • No base64url charset enforcement; any bytes pass.
  • Offset isn’t tied to the "eat_nonce" JSON key; substring could be from anywhere in payload.
-    var MAX_EAT_NONCE_B64_LENGTH = 88; // ...
+    var MAX_EAT_NONCE_B64_LENGTH = 99; // 74 bytes → 99 base64url chars (spec), or lower if you intend 64 bytes
@@
-    component length_max_check = LessEqThan(log2Ceil(MAX_EAT_NONCE_B64_LENGTH));
-    length_max_check.in[0] <== eat_nonce_0_b64_length;
-    length_max_check.in[1] <== 99;
+    component length_max_check = LessEqThan(log2Ceil(MAX_EAT_NONCE_B64_LENGTH));
+    length_max_check.in[0] <== eat_nonce_0_b64_length;
+    length_max_check.in[1] <== MAX_EAT_NONCE_B64_LENGTH;
     length_max_check.out === 1;

Additionally:

  • Use zk‑regex to enforce base64url: ^[A-Za-z0-9_-]+$.
  • Match the JSON key context (e.g., a small regex around "eat_nonce":[\s]*\["...]) rather than trusting an offset.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In circuits/circuits/gcp_jwt_verifier/gcp_jwt_verifier.circom around lines
145-175, the nonce length checks and provenance are incorrect and there's no
base64url charset enforcement: align the length bounds with the chosen
MAX_EAT_NONCE_B64_LENGTH (decide whether MAX_EAT_NONCE_B64_LENGTH represents
74-byte(=99-char) or 64-byte(=88-char) max and use that constant consistently
for min/max comparisons), replace the current numeric checks that misuse
log2Ceil with direct comparisons using the actual max/min constants, add a
zk‑regex check to extracted_eat_nonce enforcing base64url charset
^[A-Za-z0-9_-]+$ and non-empty, and instead of trusting eat_nonce_0_offset
alone, verify the offset/length fall inside a matched JSON context by matching
the surrounding payload bytes against a small zk‑regex that locates the
"eat_nonce" key (or minimal JSON pattern) and assert the substring lies within
that match.

Comment on lines 10 to 26
template VerifyExtractedString(MAX_LENGTH) {
signal input extracted[MAX_LENGTH]; // String extracted from payload
signal input input_string[MAX_LENGTH]; // String provided as input (padded)
signal input length; // Actual length of the string

// Verify each character matches (with padding)
component char_match[MAX_LENGTH];
for (var i = 0; i < MAX_LENGTH; i++) {
char_match[i] = GreaterThan(log2Ceil(MAX_LENGTH));
char_match[i].in[0] <== length;
char_match[i].in[1] <== i;

// If within length: extracted must equal input
// If beyond length: input must be 0 (padding)
char_match[i].out * (extracted[i] - input_string[i]) === 0;
(1 - char_match[i].out) * input_string[i] === 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add length upper‑bound and constrain extracted bytes beyond length to 0.

Without a bound, length can exceed MAX_LENGTH (fits into log2Ceil bits), and bytes in extracted[i] for i ≥ length remain unconstrained. Tighten constraints.

 template VerifyExtractedString(MAX_LENGTH) {
     signal input extracted[MAX_LENGTH];  // String extracted from payload
     signal input input_string[MAX_LENGTH];  // String provided as input (padded)
     signal input length;  // Actual length of the string

+    // Enforce 0 <= length <= MAX_LENGTH
+    component length_max = LessEqThan(log2Ceil(MAX_LENGTH));
+    length_max.in[0] <== length;
+    length_max.in[1] <== MAX_LENGTH;
+    length_max.out === 1;

     // Verify each character matches (with padding)
     component char_match[MAX_LENGTH];
     for (var i = 0; i < MAX_LENGTH; i++) {
         char_match[i] = GreaterThan(log2Ceil(MAX_LENGTH));
         char_match[i].in[0] <== length;
         char_match[i].in[1] <== i;

         // If within length: extracted must equal input
         // If beyond length: input must be 0 (padding)
         char_match[i].out * (extracted[i] - input_string[i]) === 0;
         (1 - char_match[i].out) * input_string[i] === 0;
+        // Also force extracted padding to zero to avoid unconstrained witness bytes
+        (1 - char_match[i].out) * extracted[i] === 0;
     }
 }
🤖 Prompt for AI Agents
In circuits/circuits/utils/gcp_jwt/verifyExtractedString.circom around lines 10
to 26, add an explicit upper-bound on length and constrain extracted bytes
beyond length to zero: ensure length is constrained to the valid range (0 <=
length <= MAX_LENGTH) by adding a comparator/constraint that enforces length <=
MAX_LENGTH (and optionally non-negative via bit-size or comparator), and inside
the loop add the complementary conditional so that when i >= length you enforce
extracted[i] === 0 while keeping the existing checks for i < length
(extracted[i] === input_string[i]) — this prevents length from exceeding
MAX_LENGTH and ensures bytes past length are constrained to 0.

eat_nonce_0_b64_output_length <== eat_nonce_0_b64_length;

// Extract image digest from payload
signal extracted_image_digest[MAX_IMAGE_DIGEST_LENGTH] <== SelectSubArray(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you check the prefix here too?

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on November 17

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

colon_after_key === 58; // ASCII code for ':'

// Verify value comes after colon (prevents pointing to colon itself)
component offset_check = GreaterThan(log2Ceil(maxJSONLength));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just check if they're equal here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON allows whitespace, so strict equality would incorrectly reject valid JSON with minor formatting differences. Currently, we use a loose GreaterThan check but we can tighten this by enforcing a closer match. For example, by measuring structural similarity within a reasonable threshold

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's check if value offset is the next index or the next to next one (if there's a space) and leave it at that

@0x471 0x471 requested a review from Nesopie November 6, 2025 11:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9c0e57 and 109789a.

📒 Files selected for processing (1)
  • circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
circuits/**/*.circom

⚙️ CodeRabbit configuration file

circuits/**/*.circom: Review ZK circuit code for:

  • Circuit correctness and completeness
  • Constraint efficiency
  • Input validation
  • Security considerations for zero-knowledge proofs

Files:

  • circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom
🔇 Additional comments (3)
circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom (3)

34-69: Strong improvements to JSON structure validation.

The key_offset underflow protection (lines 34-38), colon validation (lines 67-69), and the quote/key validation logic effectively address the previous security concerns about quote offset underflow and missing structural validation.


71-109: Excellent array structure enforcement.

The two-pattern validation (:[" vs. : [") with strict value_offset enforcement successfully addresses previous concerns about JSON array structure bypass and unsafe value extraction. The constraint at line 109 ensures value_offset cannot be arbitrarily chosen by the prover.


41-41: ItemAtIndex performs internal bounds checking—concern is mitigated but code clarity could improve.

ItemAtIndex enforces bounds by comparing the requested index against the array length and using a conditional multiplexer to either output the selected element when index < maxArrayLen or output zero otherwise. The code already validates key_offset >= 1 before the line 41 access. However, the remaining ItemAtIndex calls at lines 64, 75, 94, and 102 rely on implicit constraints through character matching (quotes, colons, brackets) rather than explicit bounds checks.

In a zero-knowledge circuit, returning zero for out-of-bounds is safe—the prover cannot exploit this to hide invalid witnesses. The character assertions provide indirect validation that offsets land within the JSON structure. Still, adding explicit bounds validation for derived offsets (like key_offset + key_length and colon_position + N) would strengthen code clarity and defense-in-depth, particularly for maintainability.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 109789a and fcb40e9.

📒 Files selected for processing (1)
  • circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
circuits/**/*.circom

⚙️ CodeRabbit configuration file

circuits/**/*.circom: Review ZK circuit code for:

  • Circuit correctness and completeness
  • Constraint efficiency
  • Input validation
  • Security considerations for zero-knowledge proofs

Files:

  • circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom
🧠 Learnings (1)
📚 Learning: 2025-09-01T13:28:11.251Z
Learnt from: mitschabaude
Repo: selfxyz/self PR: 949
File: circuits/circuits/register/register_aadhaar.circom:39-47
Timestamp: 2025-09-01T13:28:11.251Z
Learning: In zk-email-verify's Sha256Bytes circuit, paddedInLength is already constrained to be ≤ maxDataLength, so additional bounds checking for this parameter is not needed when using this component.

Applied to files:

  • circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom
🔇 Additional comments (2)
circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom (2)

34-42: Good: Underflow protection added.

The key_offset >= 1 constraint properly prevents the underflow issue flagged in previous reviews. The subsequent access at key_offset - 1 is now safe.


71-109: Good JSON structure validation, but verify bounds for intermediate positions.

The pattern-matching logic for "key":["value"] vs "key": ["value"] is well-designed and addresses previous structural validation concerns. The enforcement of value_offset based on the detected pattern prevents offset manipulation attacks.

However, colon_position + 2 and colon_position + 3 (lines 94, 102) could also exceed maxJSONLength. Consider adding bounds validation for these intermediate positions as well, similar to the suggestions for other offset calculations.

Comment on lines +26 to +28
signal input key_length;
signal input value_offset;
signal input value_length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing validation on length inputs.

key_length and value_length are accepted as inputs without constraints. A prover could set these to 0 or values exceeding their respective maximums, causing out-of-bounds access in subsequent ItemAtIndex calls (e.g., lines 64, 118-122).

Add range validation for both lengths:

    signal input key_offset;
    signal input key_length;
    signal input value_offset;
    signal input value_length;

+   // Validate key_length is within bounds and non-zero
+   component key_length_valid = LessEqThan(log2Ceil(maxKeyNameLength));
+   key_length_valid.in[0] <== key_length;
+   key_length_valid.in[1] <== maxKeyNameLength;
+   key_length_valid.out === 1;
+   component key_length_nonzero = GreaterThan(log2Ceil(maxKeyNameLength));
+   key_length_nonzero.in[0] <== key_length;
+   key_length_nonzero.in[1] <== 0;
+   key_length_nonzero.out === 1;
+
+   // Validate value_length is within bounds and non-zero
+   component value_length_valid = LessEqThan(log2Ceil(maxValueLength));
+   value_length_valid.in[0] <== value_length;
+   value_length_valid.in[1] <== maxValueLength;
+   value_length_valid.out === 1;
+   component value_length_nonzero = GreaterThan(log2Ceil(maxValueLength));
+   value_length_nonzero.in[0] <== value_length;
+   value_length_nonzero.in[1] <== 0;
+   value_length_nonzero.out === 1;

Comment on lines +63 to +69
// Verify closing quote after key
signal key_quote_after <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length);
key_quote_after === 34; // ASCII code for "

// Verify colon after closing quote (ensures valid JSON key:value structure)
signal colon_after_key <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length + 1);
colon_after_key === 58; // ASCII code for ':'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing bounds validation on key position calculations.

While the colon validation addresses previous concerns, key_offset + key_length + 1 could exceed maxJSONLength, causing undefined behavior in ItemAtIndex. In field arithmetic, out-of-bounds access may wrap around, allowing malformed JSON to pass validation.

Add bounds validation before these checks:

+   // Validate key position is within bounds
+   component key_end_bounds = LessThan(log2Ceil(maxJSONLength));
+   key_end_bounds.in[0] <== key_offset + key_length + 1;
+   key_end_bounds.in[1] <== maxJSONLength;
+   key_end_bounds.out === 1;
+
    // Verify closing quote after key
    signal key_quote_after <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Verify closing quote after key
signal key_quote_after <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length);
key_quote_after === 34; // ASCII code for "
// Verify colon after closing quote (ensures valid JSON key:value structure)
signal colon_after_key <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length + 1);
colon_after_key === 58; // ASCII code for ':'
// Validate key position is within bounds
component key_end_bounds = LessThan(log2Ceil(maxJSONLength));
key_end_bounds.in[0] <== key_offset + key_length + 1;
key_end_bounds.in[1] <== maxJSONLength;
key_end_bounds.out === 1;
// Verify closing quote after key
signal key_quote_after <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length);
key_quote_after === 34; // ASCII code for "
// Verify colon after closing quote (ensures valid JSON key:value structure)
signal colon_after_key <== ItemAtIndex(maxJSONLength)(json, key_offset + key_length + 1);
colon_after_key === 58; // ASCII code for ':'
🤖 Prompt for AI Agents
In circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom around lines
63-69, the code directly indexes ItemAtIndex(maxJSONLength)(json, key_offset +
key_length) and +1 without validating bounds; add explicit range checks for both
key_offset + key_length and key_offset + key_length + 1 against maxJSONLength
(e.g. compute endIndex = key_offset + key_length and endIndexPlusOne = endIndex
+ 1, use a LessThan comparator to assert endIndex < maxJSONLength and
endIndexPlusOne < maxJSONLength, convert those boolean signals to constraints
and then only call ItemAtIndex when the checks pass), and if the checks fail
produce a validation failure signal (or constrain to 0/1) so out-of-bounds
access cannot occur or wrap around.

Comment on lines +117 to +122
// Validate value ends with closing quote and bracket: "value"]
signal closing_quote <== ItemAtIndex(maxJSONLength)(json, value_offset + value_length);
closing_quote === 34; // ASCII code for "

signal closing_bracket <== ItemAtIndex(maxJSONLength)(json, value_offset + value_length + 1);
closing_bracket === 93; // ASCII code for ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing bounds validation on value end position.

The closing quote and bracket validation correctly checks the structure, but value_offset + value_length + 1 could exceed maxJSONLength, causing out-of-bounds access in ItemAtIndex.

Add bounds validation:

+   // Validate value end position is within bounds
+   component value_end_bounds = LessThan(log2Ceil(maxJSONLength));
+   value_end_bounds.in[0] <== value_offset + value_length + 1;
+   value_end_bounds.in[1] <== maxJSONLength;
+   value_end_bounds.out === 1;
+
    // Validate value ends with closing quote and bracket: "value"]
    signal closing_quote <== ItemAtIndex(maxJSONLength)(json, value_offset + value_length);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In circuits/circuits/utils/gcp_jwt/verifyJSONFieldExtraction.circom around lines
117 to 122, the code reads json at value_offset + value_length and +1 without
ensuring those indices are within maxJSONLength; add explicit bounds checks
(signals/assertions) that value_offset + value_length < maxJSONLength and
value_offset + value_length + 1 < maxJSONLength before calling ItemAtIndex, and
use those checks to guard or conditionally require the closing_quote and
closing_bracket comparisons so no out-of-bounds access can occur.

Copy link
Collaborator

@Nesopie Nesopie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Nesopie Nesopie changed the base branch from staging to dev November 10, 2025 14:26
@gitguardian
Copy link

gitguardian bot commented Nov 10, 2025

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
19414827 Triggered Generic Password 12bdd72 packages/mobile-sdk-alpha/ios/Frameworks/NFCPassportReader.xcframework/ios-arm64/NFCPassportReader.framework/Modules/NFCPassportReader.swiftmodule/arm64-apple-ios.swiftinterface View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Nesopie Nesopie merged commit 90ccfe4 into selfxyz:dev Nov 10, 2025
2 of 6 checks passed
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.

3 participants