Skip to content

feat(testkit): dapp-author testing tools — local-key Nido accounts + local auth simulation - #189

Open
willemneal wants to merge 2 commits into
mainfrom
feat/testkit
Open

feat(testkit): dapp-author testing tools — local-key Nido accounts + local auth simulation#189
willemneal wants to merge 2 commits into
mainfrom
feat/testkit

Conversation

@willemneal

Copy link
Copy Markdown
Contributor

Implements #188. Lets a dapp author create and exercise a Nido smart account from local keys — no WebAuthn passkey, no live network — and simulate authorization locally.

TS — @nidohq/testkit (packages/testkit)

  • Local signers for every verifier, each signing the auth digest with the bytes that verifier accepts:
    • secp256r1 → a local P-256 key building a WebAuthn assertion (the same webauthn-verifier, minus the passkey), via a ported buildSyntheticAssertion;
    • ed25519 → 64-byte signature;
    • ml-dsa-65 → post-quantum signature (@noble/post-quantum).
  • createLocalAccount — real deployer+salt C-address derivation + the account's perch PolicyDoc + a real doc_hash (perch canonical vendored, byte-identical).
  • simulateCheckAuth — mirrors do_check_auth + perch policy evaluation (function allowlist, arg predicates, expiry, N-of-N signer floor) → Kleene verdict with a readable trace.
  • reachableCalls / isNarrowing — perch reachable-call + attenuation analysis.
  • 12 vitest tests — all three signers round-trip; account derivation; the ci-publish allow/deny/expiry/is-self/zero-sig matrix; attenuation.

Rust — nido-testkit (crates/testkit)

  • test_p256_key + build_contract_assertion — the contract-test twin of the TS secp256r1 path (deterministic local key + synthetic WebAuthn assertion), pure-crypto, no soroban dep. 2 tests.

Honest scope

Only secp256r1 has a deployed verifier today. ed25519 and ML-DSA verifiers, and perch policies themselves, are modelled ahead of their on-chain contracts (ML-DSA groundwork = #143; perch = stellar-registry/perch) — the simulator is the perch interpreter until integration lands. This is what "simulation-first" is for.

Roadmap: swap the TS simulation for soroban-env in the browser (wasmi) + rs-soroban-sdk#1657's local-storage cache for lazy testnet pulls — same simulateCheckAuth call, real VM, offline-first.

Verification

  • packages/testkit: tsc --noEmit clean, tsc (dist) clean, vitest run — 12 passed
  • crates/testkit: cargo test -p nido-testkit — 2 passed

Next

Consumed + demonstrated by examples/perch-authz-console (separate PR): local-key wallet login, create a Nido account that supports perch, and a live policy visualizer + builder including the ML-DSA signer.

🤖 Generated with Claude Code

…local auth simulation

Implements #188. Lets a dapp author create and exercise a Nido smart
account from local keys — no WebAuthn passkey, no live network — and simulate
authorization locally.

TS — @nidohq/testkit (packages/testkit):
- local signers for every verifier the account supports, each signing the auth
  digest with the bytes that verifier accepts:
  - secp256r1 → a local P-256 key building a WebAuthn assertion (the same
    webauthn-verifier, minus the passkey), via a ported buildSyntheticAssertion;
  - ed25519 → 64-byte signature;
  - ml-dsa-65 → post-quantum signature (@noble/post-quantum).
- createLocalAccount: real deployer+salt C-address derivation + the account's
  perch PolicyDoc + a real doc_hash (perch canonical vendored, byte-identical).
- simulateCheckAuth: mirrors do_check_auth + perch policy evaluation (function
  allowlist, arg predicates, expiry, N-of-N signer floor) → Kleene verdict with
  a readable trace.
- reachableCalls / isNarrowing: perch reachable-call + attenuation analysis.
- 12 vitest tests (all three signers round-trip; account derivation; the
  ci-publish allow/deny/expiry/is-self/zero-sig matrix; attenuation).

Rust — nido-testkit (crates/testkit):
- test_p256_key + build_contract_assertion: the contract-test twin of the TS
  secp256r1 path (deterministic local key + synthetic WebAuthn assertion),
  pure-crypto, no soroban dep. 2 tests.

Honesty: only secp256r1 has a deployed verifier today. ed25519 and ML-DSA
verifiers, and perch policies themselves, are modelled ahead of their on-chain
contracts (ML-DSA groundwork = #143; perch = stellar-registry/perch) — the
simulator IS the perch interpreter until integration lands. Roadmap: swap the TS
simulation for soroban-env in the browser (wasmi) + rs-soroban-sdk#1657's
local-storage cache for lazy testnet pulls.

Consumed by examples/perch-authz-console (next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Example dApp preview deployed!

https://example-pr-189.mysoroban.pages.dev

The status-message example (testnet), wallet = THIS PR's preview (https://189.nido.fyi). The live home is https://nidohq.github.io/nido/ once merged.

@github-actions

Copy link
Copy Markdown

Preview deployed!

https://189.nido.fyi

Account URLs use numeric preview suffixes, for example <contract-address>--189.nido.fyi.

…st the first

A call can fall under more than one rule of the same scope (e.g. two self-admin
rules signed by different keys). The old first-match logic denied a call the
first rule couldn't satisfy even when a later rule could — so a self-admin op
signed only by the ML-DSA key was wrongly denied because admin-root (needing the
admin key) matched first.

Now it tries all scope-matching rules and authorizes if any does (matching OZ,
where the caller nominates a rule via context_rule_ids), else returns the first
deny. +2 tests: a self-admin call authorized by a non-first rule (the ML-DSA
signer), and denied when no matching rule is satisfied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
let firstDeny: SimResult | null = null;
for (const [rule, idx] of matching) {
const res = evalRule(account, ctx, ledger, rule, idx, signedBy);
if (res.verdict === 'allow') return res;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium Severity severity

Business Logic: Over-permissive rule selection in simulateCheckAuth

simulateCheckAuth now authorizes if any scope-matching rule passes (for (const [rule, idx] of matching) ... if (res.verdict === 'allow') return res), instead of selecting a single applicable rule. If multiple rules can match the same call (e.g., multiple self-admin rules), the simulator may return allow even when a dapp intended to model a specific rule selection via context_rule_ids, potentially causing developers to ship policies/UI flows that assume weaker signer requirements than what an on-chain __check_auth invocation with fixed context_rule_ids would enforce.

Make the simulator accept an explicit contextRuleId/context_rule_ids input (or a mode flag) and evaluate only that rule, keeping the current "try all" behavior as an opt-in helper for UX discovery rather than the default authorization decision.


Actions
  • Reply /almanax ask <question> to ask a follow-up question.
  • Reply /almanax dismiss [<reason>] and it won't appear again in future scans.
  • Reply /almanax resolve [<reason>] to mark the finding as resolved.
  • Reply /almanax severity <level> [<reason>] to override the severity.

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.

1 participant