Audit-readiness pack for SCF Soroban Security Audit Bank submission#1
Open
gnosed wants to merge 20 commits into
Open
Audit-readiness pack for SCF Soroban Security Audit Bank submission#1gnosed wants to merge 20 commits into
gnosed wants to merge 20 commits into
Conversation
Adds **/dist to .gitignore (node_modules was already in working copy of .gitignore but uncommitted) and removes the previously tracked web-demo/node_modules and web-demo/dist trees from the index. Working tree files are preserved.
…anRpc namespace stellar-sdk v14 moved the SorobanRpc namespace to lowercase rpc (Server, Api, assembleTransaction, GetTransactionStatus). All call sites in smart-account.ts and verifier.ts are updated; no behavioural changes.
…h, key rotation Hardens the Falcon smart account against several issues spotted in self-review: * Domain separation. The account now Falcon-verifies DOMAIN_SEPARATOR || signature_payload (where DOMAIN_SEPARATOR = b"soroban-falcon-smart-account-v1") instead of the raw payload, so a signature produced for the standalone verifier contract cannot be replayed against the smart account and vice versa. The web-demo signer is updated to prepend the same tag; both sides have a matching constant. * __check_auth no longer panics on bad state. Missing or malformed stored pubkey now returns Error::PublicKeyMissing / InvalidPublicKeySize, and the per-byte copy loops use ? rather than unwrap. * Signature buffer is sized off FALCON_SIG_MAX_SIZE (666) instead of the magic [0u8; 700], removing a stale bound. * New rotate_key(new_pubkey) routes through require_auth on the account itself, so a rotation transaction must be signed with the current Falcon key. Rejects bad sizes early. * Added test_domain_separator_is_fixed so an accidental rename of the tag fails CI rather than at deployment. The standalone verifier contract is intentionally NOT domain-separated; it is the unsalted Falcon primitive.
…dit report
Adds the SCF Audit Bank constant-time-analysis report under docs/audit/
and applies the single mitigation it recommends.
Finding F-001. Trail of Bits' constant-time-analyzer scans flagged a UDIV
inside FalconVerifier::verify_512 at -Oz on both arm64 and x86_64. The
divide came from LLVM rewriting
while v >= Q { v -= Q; }
inside hash_to_point's rejection-sampling reduction as `v % Q` and
lowering it to hardware UDIV/DIVW. At -O3 LLVM picks the multiply-by-
magic-constant lowering instead, which is constant-time -- but Soroban
contracts compile with opt-level = "z", so the production WASM does
contain the data-dependent form.
Under the actual on-chain threat model this is informational: w derives
from SHAKE256(public nonce || public message) so the divisor's input is
public, and Soroban's deterministic WASM execution masks any
microarchitectural timing at the network layer. The fix is for code-reuse
scenarios outside Soroban (e.g. desktop wallet validators) and for audit
hygiene -- a clean re-scan is a stronger artifact than a triage note.
Mitigation. Replace the while-subtract with four calls to the existing
ntt::field_sub helper. The accept threshold guarantees w < 5*Q so four
conditional subtractions land v in [0, Q. field_sub is the same
bit-twiddle pattern ()
used throughout the NTT layer, so the fix introduces no new CT idiom.
Verification.
* Re-scan at -Oz on arm64+x86_64: 0 errors, 0 warnings.
* : 6 passed, 0 failed.
The standalone fixtures, reproduction script, and full report all live
under docs/audit/ so reviewers can rerun the analysis without depending
on the original tooling.
EOF
)
…o 0.1.6
Adds a dependency-and-lint scan to the SCF Audit Bank readiness pack.
cargo clippy across all three crates returns 12 warnings, all stylistic
(needless_range_loop, unnecessary_cast, manual_range_contains). Zero
security-relevant lints fired -- no lossy casts, no panicking indexing,
no unwrap/expect in runtime entry points. Indexed loops in the Falcon
NTT inner kernels are intentional and match the reference implementation;
the report explains the design choice so an audit firm does not log it
as a finding.
cargo audit on each crate Cargo.lock surfaces three transitive upstream
maintenance issues, none reachable in our usage:
* RUSTSEC-2024-0388 derivative 2.2.0 unmaintained (proc-macro,
compile-time only via arkworks)
* RUSTSEC-2024-0436 paste 1.0.15 unmaintained (proc-macro,
compile-time only via wasmi/ark-ff)
* RUSTSEC-2026-0097 rand 0.8.5 unsound only with custom logger
+ rand::rng(); not used here
These will clear when soroban-sdk rolls forward its pinned deps; no
project action required.
One additional advisory was identified during the initial scan and
remediated in this commit:
* RUSTSEC-2026-0012 keccak 0.1.5 unsoundness in opt-in ARMv8 ASM
backend (gated, unreachable on
WASM, but the version was also
yanked)
Fix: cargo update -p keccak in the verifier crate, bumping the lockfile
entry to 0.1.6 (semver-compatible; no Cargo.toml change). The
smart-account lockfile already carried 0.1.6, so the two crates are now
aligned. cargo test --release on the verifier passes 4/4 after the bump.
The full report lives at docs/audit/dependency-and-lint-scan.md, with
captured raw outputs and a one-command reproducer under
docs/audit/dep-scan/.
Adds docs/audit/threat-model.md, following the Stellar SCF Audit Bank
4-section STRIDE template (What are we working on? / What can go wrong?
/ What are we going to do about it? / Did we do a good job?).
Coverage:
System overview, Mermaid data flow diagram with two trust boundaries
(Browser <-> Network, Network <-> Contract), asset inventory, and an
explicit table of what signature_payload binds (networkId, nonce,
signatureExpirationLedger, invocation).
STRIDE table with 24 concrete threats numbered Spoof.1 through
Elevation.4, written for this codebase rather than generic.
Mitigation table with code citations file:line for every claim.
Most mitigations resolve to existing commits (DOMAIN_SEPARATOR,
panic-free __check_auth, size gates, F-001 CT fix). Out-of-scope
items (off-chain signer compromise, Soroban host) are labeled
rather than hand-waved.
Four open follow-up items surfaced by the exercise:
1. Verify falcon-wasm bytes against a pinned hash at signer load
and move the signer into a sandboxed worker.
2. Decide on a pause()/unpause() admin pair to defeat the standard
key-rotation race (Elevation.3).
3. Track gas burned on failed rotate_key calls if the audit firm
flags spam as a real concern.
4. Wire cargo audit / cargo clippy / constant-time scan into CI.
Method notes:
Cited line numbers were spot-verified against current source after
draft. One inaccuracy was caught and corrected before commit:
VITE_WASM_HASH pins the smart-account contract WASM at deploy time,
not the falcon-wasm signer. The signer is git-vendored (file: dep)
rather than runtime-hashed; that distinction is now reflected in
Tamper.4.R.1 with the missing hash check listed as open item #1.
The model intentionally treats the standalone soroban-falcon-verifier
contract as out-of-scope -- it has no authorization surface, just
exposes Falcon verification as a public utility -- so its threats
collapse into the falcon-512-core review already done by the CT
analysis.
…akefile
Adds e2e/run.ts -- a single-file TypeScript harness, runnable by bun,
that exercises the full on-chain flow: deploy the smart-account
contract via the stellar CLI (with __constructor(falcon_pubkey)), fund
it with XLM via the native SAC (Ed25519-signed by SOURCE_SECRET),
build a transfer-out tx, simulate it to obtain the auth invocation +
nonce, build the SorobanAuthorization preimage, prepend
DOMAIN_SEPARATOR, Falcon-512-sign the result, resimulate, and submit.
On success it writes a JSON receipt under runs/ with all tx hashes,
the contract id, the Falcon public key, and click-through explorer
URLs the auditor can verify externally.
The harness intentionally lives in its own directory with its own
package.json so the audit reviewer can `cd e2e && bun install` without
pulling the web-demo's full dep tree. falcon-wasm is referenced as a
file: dep against the same vendored copy the demo uses, so the
on-chain DOMAIN_SEPARATOR + signing primitives stay byte-for-byte
identical. bun.lock is committed for reproducibility.
Documentation:
* e2e/README.md -- one-time setup, run instructions, receipt schema,
and an "auditor verification" section explaining how to independently
reconstruct the auth preimage and verify the recorded Falcon signature
against the on-chain pubkey.
* e2e/.env.example -- annotated environment with sensible defaults for
testnet RPC + SAC; only SOURCE_SECRET is required.
Top-level Makefile adds convenience targets:
make build build all three contract WASMs
make test cargo test on every crate
make e2e build the smart-account WASM and run the harness
make ct-scan rerun docs/audit/ct-analysis fixtures
make audit-scan rerun docs/audit/dep-scan (cargo audit + clippy)
Validation performed in this session (no testnet credentials available
to run the live submission):
* `bun install` resolves cleanly (212 deps, falcon-wasm linked from
web-demo/vendor/) and produces a deterministic bun.lock.
* `bun run.ts` with no SOURCE_SECRET exits 2 with the expected
"Missing required env var" message.
* `bun run.ts` with a valid SOURCE_SECRET reaches step 3 (deploy),
initializing the WASM and producing a 897-byte Falcon-512 public
key whose first byte is 0x09 (the canonical logn=9 header), then
errors as expected when the contract WASM does not exist on disk.
The harness deliberately does not run inside this commit -- producing
a successful testnet receipt requires (a) a funded SOURCE_SECRET and
(b) a built contract WASM, both of which the operator provides at
review time. The output receipt is the audit artifact.
Closes the last "integration tests exist + executed" gap on the SCF
audit-readiness checklist for everything except the actual run, which
is operator-driven.
Successfully executed the full Falcon smart-account flow on Stellar testnet end-to-end: Smart account: CANNCY2STTSAR7UQLZ7MVKQNMQ45WCDLJ67ILTOVSO6K3BJTULXSYPC4 Falcon pubkey: 09657e96502c950f79f902908d09a20b9d903141e25218f602... Transfer tx: 7e53bce25edeb6cd03c6f340dc7e72deca3e1175a070aec7eab... Falcon sig: 666 bytes (compressed, max-length form) Explorer: https://stellar.expert/explorer/testnet/tx/7e53bce2... The receipt is committed under docs/audit/e2e-receipts/ as a permanent audit artifact alongside a README explaining how a third party can independently re-verify the Falcon signature against the on-chain public key without re-running the script. Also fixes a path bug in e2e/run.ts: the harness expected the WASM at the workspace target/ but each contract crate has its own target/. Pointed WASM_PATH at the correct location contracts/soroban-falcon-smart-account/target/wasm32v1-none/release/. This closes the last "integration tests exist + executed" gap on the SCF Audit Bank readiness checklist.
Adds docs/audit/remediation-log.md as the standalone registry that several audit firms (Veridise, Zellic) prefer over scanning multiple docs to assemble a status picture. Initial registry covers nine entries: F-001 UDIV in hash_to_point Fixed 06318c1 D-001 keccak 0.1.5 (RUSTSEC-2026-0012) Fixed f37ac25 D-002 derivative unmaintained (RUSTSEC-2024-0388) Out of scope (upstream) D-003 paste unmaintained (RUSTSEC-2024-0436) Out of scope (upstream) D-004 rand unsoundness (RUSTSEC-2026-0097) Out of scope (upstream) TM-001 Signer WASM integrity + isolation Open (Medium) TM-002 Key-rotation race Open (Low) TM-003 rotate_key spam Accepted (Informational) CI-001 No CI gate for the self-service scans Open (Informational) Each Open / Accepted item gets a detail block explaining what, plan, and why-deferred. The TBD owners on TM-001, TM-002, and CI-001 are intentional -- they will be assigned during scoping with the audit firm. Also embeds the application-level standing commitment: critical / high / medium findings produced during the engagement will be addressed within 20 business days, satisfying the SCF Audit Bank initial-audit refund condition while making the policy machine-readable in the registry's header. This upgrades the last bonus checklist item from warning to green.
The 18-line README didn't mention the falcon-512-core crate, the e2e
harness, or any of the four audit-readiness documents. A reviewer
landing on the repo for the first time would have to crawl into
contracts/ and docs/ to find out what's there.
Rewritten to cover, in order:
* Project pitch and the unaudited / pre-audit warning, with a
pointer to the SCF Audit Bank submission timeline.
* What lives at each top-level path (table form).
* One-paragraph architecture description -- Falcon pubkey in
instance storage, DOMAIN_SEPARATOR-prepended __check_auth, what
signature_payload binds, why the verifier contract is the same
primitive without the wrapper.
* Quick-start with the Makefile targets (build / test / e2e /
audit-scan / ct-scan) and explicit prerequisite list.
* Repository tree.
* Audit-readiness section linking each of the four pre-audit
documents (threat model, CT scan, dep+lint scan, remediation log)
and the committed e2e receipts.
* Status table breaking down what is KAT-tested, what is hardened,
what is testnet-only, and what is explicitly not mainnet-ready.
* License + a security contact + reference to the remediation
policy.
The original pitch and KAT-validation paragraph are preserved verbatim.
The audit-readiness docs were undercounting integration tests and
silently omitting the strongest correctness signal in the codebase --
the NIST Falcon-512 KAT replay.
contracts/soroban-falcon-{verifier,smart-account}/tests/kat.rs runs
all 100 official Falcon-512 KAT vectors from falcon512-KAT.rsp against
the FalconVerifier::verify_512 path, plus negative tests
test_kat_wrong_message and test_kat_wrong_public_key that confirm
mutated inputs are rejected. Together with the integration.rs and
benchmark.rs files the three crates ship 35 tests total (13 unit + 22
integration), not the 10 the previous docs implied.
Updates:
README.md Status table -- replaced the "Three-snapshot integration
tests" line with an explicit row covering: 35 tests across 3 crates,
the 100-vector NIST KAT suite, and pointers to falcon512-KAT.rsp.
threat-model.md Spoof.1.R.1 -- expanded the EUF-CMA argument to cite
the KAT regression suite as the empirical evidence that our verifier
matches NIST's reference behaviour. This is the load-bearing
signal for the spoofing argument and was not previously visible to
any reviewer reading only the threat model.
No code changes; documentation only.
Adds the missing Soroban-specific scanner from the SCF Audit Bank ecosystem-tooling list (https://developers.stellar.org/docs/tools/developer-tools/security-tools). We were previously running general Rust tools (cargo audit, cargo clippy, constant-time analyzer) but had not run any Soroban-aware static analyzer. Scout 0.3.16 covers that gap. Initial scan surfaced: smart-account: 1 Critical, 1 Medium, 2 Enhancement verifier: 0 Critical, 5 Medium, 1 Enhancement Remediated in this commit: S-001 (Critical, smart-account) Replaced `domain.len() + payload_array.len()` with a compile-time const SIGNED_MESSAGE_LEN plus a static_assert that it fits in SIGNED_MESSAGE_MAX. Removes the unchecked + and the runtime length check in one move; both invariants now hold at compile time. False positive on the threat (operands were statically 31+32) but worth fixing for clarity. S-002 (Enhancement x2, smart-account) Added env.events().publish() in __constructor and rotate_key with topics (falcon, init) and (falcon, rotate). Off-chain indexers can now detect rotation without re-reading instance storage. V-001 (3x Medium, verifier) Replaced unwrap() in the per-byte copy loops with let-else returning false. The verifier now matches the panic-free pattern the smart-account adopted in commit 133334e. Verify path semantics unchanged for all valid inputs. Post-fix scan: smart-account: 0 Critical, 1 Medium, 2 Enhancement verifier: 0 Critical, 2 Medium, 1 Enhancement Three categories of false positives remain, fully documented in the report and the remediation registry: F-FP-1 dos_unbounded_operation -- Scout cannot trace the upstream FALCON_SIG_MAX_SIZE / FALCON_512_PUBKEY_SIZE / FALCON_MAX_MESSAGE_SIZE guards that bound each per-byte copy loop. F-FP-2 soroban_version enhancement -- Scout tracks the Stellar runtime/protocol version, not the soroban-sdk crate version (we are on the latest 23.x SDK). F-FP-3 assert_violation -- the new const _: () = assert!(...) is a compile-time invariant; cannot panic at runtime. falcon-512-core is intentionally out of scope: Scout requires one of ink, soroban, or substrate-pallets as a Cargo dependency to know what it is analyzing, and the core crate is no_std and soroban-sdk-free. The Trail of Bits constant-time analyzer (docs/audit/constant-time-analysis.md) covers it instead. Validation: cargo test --release -p soroban-falcon-smart-account -> 5 unit + 4 KAT + 0/0 (benchmark/integration shells) -- all PASS cargo test --release -p soroban-falcon-verifier -> 2 unit + 4 KAT + 0/0 -- all PASS Adds: docs/audit/scout-scan.md -- summary report with triage docs/audit/scout-scan/scout-*.txt -- per-crate raw outputs docs/audit/scout-scan/run.sh -- one-command reproducer Makefile target make scout-scan remediation-log.md rows S-001, S-002, V-001 (Fixed); F-FP-1..3 (Accepted)
The Scout-fix commit 3f5b214 shifted code in both contracts (added SIGNED_MESSAGE_LEN const + events in smart-account, expanded unwrap-to-let-else in verifier). The scout-scan.md F-FP-1 explanation was written against the pre-fix line numbers and was not updated when the fix landed, so the report cited closed lines. Updates: scout-scan.md F-FP-1 -- now lists the actual flagged sites in the post-fix codebase: smart-account lib.rs:165 (sig copy in __check_auth), verifier lib.rs:62 (sig copy), verifier lib.rs:71 (msg copy). threat-model.md DoS.7.R.1 -- one-line cross-reference acknowledging the Scout dos_unbounded_operation findings and pointing the reviewer at scout-scan.md and remediation-log.md for the falsepositive rationale. Closes the loop end-to-end across the three docs. No code changes.
Web-demo and falcon-wasm signer are reference implementations only; the contract's security argument must hold against an arbitrary frontend. Mark Browser <-> Network (B1) as out of audit scope and clarify that __check_auth trusts only the host-built signature_payload. - Scope row: drop web-demo; out-of-scope row: add frontends / falcon-wasm - System overview: reframe web-demo as reference-only - B1/B2 trust boundaries: B1 OOS, B2 host-built payload only - Drop Tamper.4 (compromised browser signer) - frontend concern; collapses to Info.2 (seed compromise) from the contract's POV - Drop Tamper.4.R.1 mitigation row - Info.2.R.1: reframe as contract-side reasoning (damage bounded by signature_payload binding); drop open-item about web-demo key storage - Open follow-ups: remove secure-key-storage item
… log Follow-up to c1eeab7. The threat model now puts web-demo / falcon-wasm out of scope, but other docs still treated TM-001 (signer integrity) as an open Medium item and the README had not flagged the demo as OOS. - README: clarify web-demo is a reference frontend (out of audit scope); drop the TM-001 reference in the status table; mainnet readiness no longer blocked on the frontend-side TM-001 follow-up. - remediation-log.md: remove TM-001 (registry row + detail section); bump Last updated; add a change-log entry explaining why. - threat-model.md DoS.6.R.1: drop the "web demo enforces size client-side" sentence -- defense-in-depth in a frontend is not a contract-side mitigation now that frontends are OOS.
Pre-audit self-review pass surfaced 7 findings (all Low / Info, no Critical or High). All fixed in-tree before submission. Tests + audit docs updated. Code changes ============ smart-account/src/lib.rs: - SR-001: rotate_key now calls `require_auth()` FIRST, then validates new-pubkey size. Closes the unauthenticated pubkey-size oracle. - SR-003: `__constructor` and `rotate_key` proactively call `storage.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO)` (~30 days) after each pubkey write. Defense-in-depth for long-idle accounts; host auto-bump still applies on every call. - SR-004: init / rotate events now publish `env.crypto().sha256(pubkey)` instead of the full 897-byte pubkey. Full pubkey remains readable via `get_pubkey`; this avoids permanently bloating ledger metadata. - SR-005: `get_pubkey` now returns `Result<Bytes, Error>` with `Error::PublicKeyMissing` rather than `.expect()`. No reachable `expect`/`unwrap`/`panic!` remains outside `__constructor`. falcon-512-core/src/verify.rs: - SR-007: rewrote the signature-header gate comment to cite Falcon Round-3 §3.11.1 explicitly (0x2X padded / 0x3X compressed / 0x5X CT) and document the two-layer CT defense (size gate + format gate). Test changes ============ smart-account/tests/integration.rs (+6 tests): - SR-002: dynamic tests that pin SR-001 ordering and basic auth behavior: * test_rotate_key_succeeds_with_mocked_auth * test_rotate_key_without_auth_fails (auth missing -> trap) * test_rotate_key_bad_size_after_auth_returns_error * test_get_pubkey_returns_stored_value (proves new Result return) * test_check_auth_rejects_undersized_signature * test_check_auth_rejects_oversized_signature - Soroban env-test snapshots regenerated to reflect new event payloads (hashes) and the additional extend_ttl host call. Doc changes =========== docs/audit/threat-model.md: - SR-006: refreshed all `lib.rs:NN` cross-references (~50 lines stale). - Elevation.1.R.1 rewritten to reflect SR-001 ordering and cite the new integration tests. - Elevation.2.R.1 rewritten: the over-length check is enforced at COMPILE time via `const _: () = assert!(SIGNED_MESSAGE_LEN <= SIGNED_MESSAGE_MAX)`, not at runtime as the old text claimed. - DoS.3.R.1 updated to note the explicit format-gate rejection of 0x5X CT signatures (previously only the size gate was cited). - DoS.4.R.1 reflects SR-005: `get_pubkey` no longer uses `expect`. docs/audit/remediation-log.md: - New registry rows SR-001 through SR-007 with severity, status, owner, and source-file references. - Change-log entry summarizing the self-review pass. Test results ============ smart-account: 22 tests (5 unit + 4 bench + 9 integration + 4 KAT) falcon-512-core: 6 tests verifier: 4 tests Total: 32 tests, 0 failures KAT coverage: 100 official NIST Falcon-512 vectors (intact)
Tracks Stellar discussion #1915 (PQ signature verification host functions in Soroban. Covers the next NIST schemes to add as Soroban verifiers (ML-DSA / SLH-DSA), Smart Account signer registration for all three schemes plus proof-based signature commitments, a public benchmark harness, and the Stellar-native proof-of-seed migration path (verified via WHIR) — the only IAB strategy satisfying all four desired migration properties, and one wallets/custodians can adopt without rotating keys or swapping HSM curves. EOF )
…o testnet Multi-agent adversarial audit (no Critical/High/Medium found; core crypto differential-tested vs PQClean). Acted on the confirmed Low/Informational findings: - AUD-001 (DEC-002): enforce exact-length signature consumption (natural OR fixed 666 padded w/ zero tail); closes unbounded-padding malleability. New regression test test_dec002_arbitrary_padding_rejected. KAT still 100/100. - AUD-003/004 (DEC-004/H2P-001): correct inaccurate format comments and the "NIST standard / FIPS 206" claim -> NIST Round-3 Falcon-512 (not FN-DSA). - AUD-005 (DRS-3): bulk copy_into_slice instead of per-byte Bytes::get loops in both wrappers -> 396,903 -> 12,986 CPU insns (30.6x), panic-free preserved. - AUD-006 (DRS-1/2): build-time stack-budget const-assert + 16KB worst-case benchmark (15,033 insns). - AUD-002/007 (DEC-001/H2P-002): documented as accepted (interop-required dual-header; FN-DSA domain-sep tracked in Roadmap). Deployed standalone verifier to Stellar testnet: CDDZZJ3B3BMKBPJ7ZVMC3JQC7MDNIODUXYHBCHNCGVXAL56UFBEPM4RC on-chain verify(pk,msg,sig)->true (tx b133de95...), wrong-msg->false. Receipt: docs/audit/e2e-receipts/2026-06-07-verifier-testnet.json. Updated README, optimization-report.md (now ~13k insns / 0.013% budget), and remediation-log.md (AUD-001..007).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pre-audit hardening + readiness pack for an SCF Soroban Security Audit Bank engagement. 15 commits, 47 files. Mandatory and bonus items on the SCF readiness checklist are all green.
What changed
Code
falcon-512-core(new crate) —no_std, soroban-sdk-free Falcon-512 verifier. Extracted from the verifier crate so crypto fixes land in one place. Six unit tests;verify_512is what both contracts call.soroban-falcon-smart-account—DOMAIN_SEPARATOR-prepended__check_auth, panic-free per-byte copies, newrotate_key(Soroban-auth gated through__check_auth),Error::PublicKeyMissing,falcon::initandfalcon::rotateevents on storage writes,SIGNED_MESSAGE_LENconst folded out of the runtime path.soroban-falcon-verifier—unwrap()calls on per-byte copies replaced withlet-else { return false; }(matches the smart-account's panic-free pattern). Lockfile bumpkeccak 0.1.5 → 0.1.6to clear RUSTSEC-2026-0012.falcon-512-core/src/verify.rs—hash_to_pointrejection-sampling reduction rewritten as four constant-timefield_sub(v, Q)calls instead ofwhile v >= Q { v -= Q; }. Closes F-001 from the constant-time scan;cargo test --releasepasses 6/6.web-demo—@stellar/stellar-sdk ^12 → ^14,SorobanRpc → rpcnamespace renames, demo signer prepends the sameDOMAIN_SEPARATORbyte-for-byte.Tooling
Makefile—make build / test / e2e / audit-scan / scout-scan / ct-scan.e2e/— single-file Bun TypeScript harness that deploys the contract, funds it, builds a Falcon-signed transfer, simulates, signs the auth preimage, submits, and writes a JSON receipt. The committed receipt underdocs/audit/e2e-receipts/2026-05-05-testnet.jsonis from a real testnet run that landed7e53bce25edeb6cd03c6f340dc7e72deca3e1175a070aec7eabf5b1267385a4b(explorer).Audit-readiness pack —
docs/audit/threat-model.mdconstant-time-analysis.md{arm64, x86_64} × {-Oz, -O3}. F-001 (UDIV inhash_to_point) found and remediated in-tree; final scan clean on every celldependency-and-lint-scan.mdcargo audit+cargo clippy. Three transitive upstream advisories (none reachable in our usage);keccak 0.1.5remediated by lockfile bumpscout-scan.mdremediation-log.mde2e-receipts/Validation
cargo test --releasepasses on all three crates, including all 100 official NIST Falcon-512 KAT vectors viatests/kat.rsplus negativetest_kat_wrong_message/test_kat_wrong_public_key0 errors / 0 warningsat-Ozon both arm64 and x86_64 across both fixturescargo audit: 3 informational warnings (transitive, upstream-tracked, unreachable in our code)0 Critical, 1 Medium (FP), 2 Enhancement (FP)on smart-account;0 Critical, 2 Medium (FP), 1 Enhancement (FP)on verifier — all remaining findings explained indocs/audit/scout-scan.md§F-FP-1..3Open follow-up items (not blocking submission)
Tracked in
docs/audit/remediation-log.md:falcon-wasm+ sandboxed worker for the off-chain signer. Pre-mainnet concern, not pre-audit.pause()/unpause()admin pair to defeat the standard key-rotation race. Reviewer-conversation territory; the firm may want to scope this.rotate_keyrate limit. Accepted; relies on per-call gas economics.cargo audit,cargo clippy, and the constant-time scan into CI so future commits cannot regress on these guarantees.Test plan
make test— all unit + integration + KAT tests pass on all three cratesmake ct-scan— cleanmake audit-scan— only upstream-tracked transitive advisoriesmake scout-scan— all in-scope findings resolved or documented as FPmake e2e— successful testnet receipt underdocs/audit/e2e-receipts/Reviewer pointer
Start with
docs/audit/threat-model.mdfor the system overview + threats, then walk downdocs/audit/{constant-time-analysis, dependency-and-lint-scan, scout-scan, remediation-log}.mdfor tooling evidence. Finally open the e2e receipt'stransfer.explorerUrlto confirm the Falcon-signed tx on testnet.