Design check: shared tamper tests for FROST co-signers — would you run them in CI?

Before writing a ZCG application, I’d like to check whether this is wanted.

The idea: a shared, versioned set of tampered PCZTs plus a small runner. Any shielded FROST co-signer runs it in CI and checks that it refuses what the user didn’t approve: swapped recipient, changed amount, fee above ZIP 317, an unknown spend or output, change to a foreign address, redacted fields it relies on, a sighash that doesn’t match the PCZT, coordinator equivocation. Failures go privately to the maintainer first.

Background: in our 2-of-3 testnet escrow the arbiter refused a mismatched PCZT (GitHub - predgeAI/zcash-escrow: Shielded 2-of-3 FROST escrow for Zcash P2P commerce: buyer, seller, neutral arbiter. Runs on public testnet. · GitHub).

@conradoplg @Khadim @hanh @anmolbansal02 @danielgorgonha:

  1. Would you run this, or is your own suite enough?
  2. Which failure cases worry you most?
  3. Adapter over CLI (stdin/stdout) or library call?

@Khadim, we’d import your #490 cases with credit rather than duplicate them.

Hi @predge-ai, thanks for the tag. Answers from Konclave (shared vaults, FROST over PCZTs, signing in the browser).

1. Would we run it? Yes, if it is data: files plus the expected result, which we wire to our own signer in our own CI. We would keep our own tests too. One thing decides whether the vectors mean anything. A PCZT on its own cannot be “tampered”: a swapped recipient is swapped relative to what was approved. So a case needs the PCZT together with what the signer was told to expect: the approved payment (address, amount, memo), the group key, the signer’s own receivers and the block height. And each refused file should come with its accepted twin, changed in one place, so a signer that refuses everything, or refuses for the wrong reason, does not pass.

2. Cases that worried us most. These come from our own signer’s history. They are in our changelogs (project, app), most with what was open:

  • The signature share is made over a signing package whose message is not the sighash the device computed from its own PCZT. A check next to the signing call was not enough for us. It had to be inside it.
  • A spend that is not under the vault’s group key.
  • What the device shows was read from fields the signature does not cover: an output that does not match its note commitment, an address label that is not the receiver, a memo that differs from the approved one.
  • Fee. “Above ZIP 317” needs a height. Under ZIP 2009 the marginal fee is 1,000 zatoshis from mainnet block 3,590,000, so the same file is right or wrong depending on when it is mined.
  • Change paid to a receiver that is not in the device’s own record.
  • The coordinator asking for a second, different transaction for the same approval.

We do not pass everything ourselves. Our open items are under Known limits in the same file.

3. Adapter. A library call first (Rust, usable from WASM), with a thin stdin/stdout wrapper over it. The first and last cases above are not files, they are a sequence of messages, and a CLI that takes one PCZT cannot express them. I would also give each case an expected refusal class from a short shared list, and let each signer map its own errors to it.

Two notes on the design:

  • A new case in a public set tells everyone how to attack each signer that does not pass it yet. “Failures go privately to the maintainer first” covers the results. I would do the same for new cases: maintainers who run the set see a case some days before it is published.
  • Each team runs this in its own CI, so a pass is that team’s own report. Useful against regressions, and not an audit.

@Khadim, the tamper cases in your approved work (#490) are the natural start. I would rather contribute to one set than run two. Where would you want shared cases to live? We can write ours up as vectors for whichever home you two agree on, after the milestone we are measuring this month.

To be clear, this is an answer about the design. Whether any of it should be funded is for ZCG, and I am not arguing either way.

Thanks to both of you.

A tamper suite for FROST co-signers is already part of #490, approved on Nov 9. It runs on every PR in frost-zec (MIT): a swapped recipient, an address that isn’t the one paid, a note from another wallet, outputs the group can’t read back, redactions, a zero or repeated randomizer, and more. Feel free to reuse the cases.

Daniel, agreed on the design: a case is the PCZT plus what the signer was told to expect, with an accepted twin and a refusal class each signer maps its own errors to. Your two sequence cases need their own shape. frost-zec covers both by design: it computes the sighash and randomizers from the PCZT it checked, never from the coordinator, and a session answers one request, then its nonces are gone. They still deserve a case.

I’d start the shared cases in frost-zec, under vectors/, with the format written in the #490 spec so it doesn’t depend on our code. If the spec becomes a ZIP, they can move to zcash-test-vectors with it. New cases would go to the maintainers who run the set before they’re published. I’ll open an issue with a format draft once our first milestone is in.