Skip to content

Fix Citadel security audit findings#137

Open
HDauven wants to merge 3 commits into
mainfrom
security/citadel-audit-fixes
Open

Fix Citadel security audit findings#137
HDauven wants to merge 3 commits into
mainfrom
security/citadel-audit-fixes

Conversation

@HDauven

@HDauven HDauven commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

  • Harden the license ZK circuit point-validity checks against torsion-shifted witness points.
  • Validate public keys and stealth addresses before DH, signature, and proof use.
  • Pin wallet verification to trusted issuer/policy inputs and contract verifier/circuit metadata; remove argv password input and document the env/prompt path.
  • Regenerate circuit setup material in contract/build.rs, embed the verifier from OUT_DIR, and test metadata/verifier consistency.
  • Keep session-capacity policy out of the base contract and specify that uncommitted cookie binding data does not secure a bearer cookie.
  • Document the Dusk VM panic-as-revert model for contract entrypoints and the fail-fast error model for required build artifacts.

Verification

  • cargo fmt --all -- --check
  • cargo test -p zk-citadel --release --features zk
  • cargo clippy -p zk-citadel --all-targets --features zk --release -- -D warnings
  • cargo test -p zk-citadel-wallet --release
  • cargo clippy -p zk-citadel-wallet --all-targets --release -- -D warnings
  • make contract
  • make test-contract
  • cargo clippy -p license-contract --all-targets --release -- -D warnings
  • GPT 5.5 xhigh final-head red-team at 0958b47: no PR-blocking security or correctness findings.
  • controlecentrum review-pr dusk-network/citadel#137 --mode deep --blind --head-sha 0958b47 --budget-minutes 20 with GPT 5.5 xhigh: final verdict COMMENT, 0 validated findings.

Notes

  • Session lifecycle, state capacity, and replacing the linear session map remain separate deployment/design work before production.
  • Public GitHub issue creation for the remaining security findings is pending explicit disclosure approval.

@HDauven HDauven requested a review from xevisalle July 10, 2026 18:35
@HDauven HDauven force-pushed the security/citadel-audit-fixes branch from be3ddc1 to 51c201e Compare July 10, 2026 18:44

@xevisalle xevisalle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I left several comments. In general I see several protocol changes that should be explained. Also, I think the specification needs to be updated accordingly before touching the code.

Comment thread contract/src/state.rs Outdated
const MERKLE_ARITY: usize = 4;
const DEPTH: usize = 16; // the depth of LicenseCircuit's Merkle tree
const ROOT_HISTORY_SIZE: usize = 8;
const MAX_SESSIONS: usize = 1 << DEPTH;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't get this. Why is 2^16 the max number of sessions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't get this. Why is 2^16 the max number of sessions?

This was done as a defensive state bound, but tying it to the Merkle depth was arbitrary and way too constraining. I agree it should be removed

Comment thread contract/tests/license_contract.rs
Comment thread contract/build.rs Outdated
const CIRCUIT_MARKER_PATH: &str = "../target/license-circuit-build-id";
const CIRCUIT_BUILD_ID: &str = "zk-citadel-license-circuit";
const OUT_DIR_VERIFIER_FILENAME: &str = "license_verifier";
const CIRCUIT_BUILD_DOMAIN: &[u8] = b"zk-citadel-license-circuit-v2";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why v2?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why v2?

We don't really need to version the build ID as is. The fingerprint is opaque and checked by exact equality, and we do not support both the old static marker and this source derived format. I'll replace v2 with a descriptive source fingerprint domain.

Comment thread contract/build.rs
];

#[tokio::main]
async fn main() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what are the advantages of all these changes to the contract built?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

what are the advantages of all these changes to the contract built?

Prior setup files could be reused after circuit source changes because the marker was static. These changes make Cargo track the circuit sources, regenerating the prover/verifier together, embedding the verifier produced by the build and expose the fingerprints so wallets can reject mismatched artifacts.

It gives stronger guarantees

Comment thread core/src/assets/session.rs Outdated
/// Optional exact root freshness check for this policy.
pub expected_root: Option<BlsScalar>,
/// Optional exact binding data expected by this policy.
pub expected_binding_data: Option<[BlsScalar; 4]>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what's this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

what's this?

It was intended to enforce profile defined accounts or channel binding. binding_data is currently not committed though by the proof or session transcript, so checking it alone does not provide any cryptographic binding. Question is if we want to do it or not, as it was flagged as a risk area

Comment thread core/src/zk/gadgets.rs Outdated
assert_not_identity(composer, eight_p);
// The cofactor map [8] has the prime-order subgroup as its image. Proving
// point = [8]Q for an on-curve Q excludes hidden torsion components.
let inv_eight = JubJubScalar::from(8u64).invert().unwrap();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isn't this underconstrained? and, what's the reason of the added complexity?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

isn't this underconstrained? and, what's the reason of the added complexity?

This was added because the old check didn't catch all invalid points. It rejected points that were fully low order, but not a valid point with a low-order part added. The test covers that case.

As I understand it, Q isn't underconstrained because the circuit checks that it is on the curve and that [8]Q = P. But I'm not attached to this implementation. If there is a simpler or more standard way to do the subgroup check here, I'd rather use that. This came about from the audit by the way

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok I'll think about it

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.

2 participants