Skip to main content
Sigvex

Private Input Unchecked

Detects Noir private (witness) inputs of type Field that are not referenced in any assertion, allowing the prover to set them to arbitrary values.

Private Input Unchecked

Overview

Remediation Guide: How to Fix Unchecked Private Inputs

The private input unchecked detector identifies Noir functions where private (witness) parameters of type Field are not referenced in any assert or constrain statement. In Noir, private inputs are witness values provided by the prover. Without constraints binding them, the prover can set these values to anything and still produce a valid proof.

This is the Noir equivalent of Circom’s under-constrained signal vulnerability. The difference is in the language model: Circom uses signal assignments and explicit constraints (===), while Noir uses assertions (assert, assert_eq) within constrained functions to bind witness values.

Why This Is an Issue

Private inputs represent the prover’s secret witness. The verifier never sees these values directly – it only verifies that the proof satisfies the circuit’s constraints. If a private Field parameter does not appear in any assertion, there is no constraint preventing the prover from setting it to any value in the field.

Consider a function that takes a private secret and a public hash, intended to prove knowledge of a preimage. If secret is never constrained against hash, the prover can submit any value for secret and claim to know the preimage. The proof verifies, but the claim is false.

How to Resolve

Add an assertion that constrains the private input’s value relative to other circuit values.

// Before: Vulnerable -- private input has no assertion
fn main(public_hash: pub Field, secret: Field) {
    let computed = std::hash::pedersen([secret]);
    // BUG: computed is never checked against public_hash
}

// After: Fixed -- private input is constrained via assertion
fn main(public_hash: pub Field, secret: Field) {
    let computed = std::hash::pedersen([secret]);
    assert(computed == public_hash);  // binds secret to public_hash
}

Examples

Vulnerable Code

fn main(x: pub Field, secret: Field) {
    let z = x + secret;
    // No assertion -- secret can be any value
    // The prover controls z entirely
}
fn verify_membership(root: pub Field, leaf: Field, path: [Field; 10]) {
    let computed_root = compute_merkle_root(leaf, path);
    // BUG: computed_root is never checked against root
    // leaf and path are unconstrained witness values
}

Fixed Code

fn main(x: pub Field, secret: Field) {
    assert(secret != 0);  // basic constraint on secret
    let z = x + secret;
    assert(z == expected_output(x));
}
fn verify_membership(root: pub Field, leaf: Field, path: [Field; 10]) {
    let computed_root = compute_merkle_root(leaf, path);
    assert(computed_root == root);  // constrains leaf and path
}

Sample Sigvex Output

CRITICAL  private-input-unchecked
  Private input `secret` in function `main` has no assertion constraint at line 1

  Private parameter `secret` (type `Field`) in function `main` is a witness value
  provided by the prover. It does not appear in any `assert` or `constrain`
  statement, meaning the prover can set it to any value and still produce a valid
  proof. This is a critical under-constraint vulnerability.

  Function: main
  Signal:   secret
  Confidence: 0.80

  Recommendation: Add an assertion that constrains this private input. For example:
    `assert(private_input == hash(public_input));`
    or use it in a constraining expression.

Detection Methodology

The detector scans each constrained function (not marked unconstrained) for private Field parameters. For each function:

  1. Identify private Field parameters: parameters not marked pub whose type is Field or contains Field (e.g., [Field; N]).
  2. Collect assertion text: concatenate all assert, assert_eq, and constrain expressions in the function body.
  3. Check references: if a private Field parameter’s name does not appear in any assertion expression, it is flagged.

The detector only checks constrained functions because unconstrained functions execute outside the circuit and are not expected to contain assertions. Only Field-type parameters are checked because they represent arbitrary finite field elements – smaller integer types (u8, u32, etc.) have implicit range constraints from their type.

Limitations

  • Name-based matching: the detector checks if the parameter name appears as a substring in assertion expressions. Aliasing (e.g., let alias = secret; assert(alias != 0);) is not tracked, which may cause false positives.
  • Does not check non-Field private inputs. Private u32 or bool inputs have implicit range constraints but may still lack semantic constraints.
  • Does not verify assertion sufficiency. An assertion like assert(secret == secret) is recognized but provides no actual constraint.
  • Confidence is 0.80, reflecting the substring-matching limitation.

References