frost: document deterministic_sign's nonce derivation domain

BIP 445's det_nonce_hash commits to the secret share, my_id, u, the
sorted ids, the aggothernonce, the x-only tweaked threshold public key
and the message. It does not commit to the pubshares, to the untweaked
threshold public key, or to the accumulated tweaks. Because Q and -Q
share an x-coordinate, two tweak caches can agree on everything the
derivation hashes and still disagree on the sign g*gacc that multiplies
the secret share -- a cache initialized from thresh_pk and one
initialized from its negation being the smallest example.

Two calls differing only in that emit the same pubnonce and partial
signatures s = k + e*lambda*d and s' = k - e*lambda*d over the identical
k = k1 + b*k2, so subtracting them yields the secret share. Demonstrated
on a sole signer (u = 1, ids = {0}, pubshares = NULL) with thresh_sk =
0x11.. and msg = 0x42..:

  tweaked pk (cache A)  4f355bdc...075871aa
  tweaked pk (cache B)  4f355bdc...075871aa   same x-only key
  pubnonce A == pubnonce B                    nonce reused
  sA - sB               0d7d9c4e...aa748ffa
  -2*e*d                0d7d9c4e...aa748ffa   d recovered

Nothing inside a single call can catch this. The self-verification that
Sign performs passes in both cases, because each partial signature is
individually valid under the cache it was produced with;
validate_session_params likewise only ties the pubshares to the cache's
own Q0, which both caches satisfy by construction. Note also that
pubshares is optional, so there need not be a second value to disagree
with.

No code change: this is the specified derivation, and committing to Q0
or to gacc here would diverge from BIP 445 and invalidate the
det_sign test vectors. The obligation is the caller's, so state it where
the caller will meet it -- in the function's own documentation and
alongside the existing secnonce and session_secrand32 rules in frost.md.
The rule is that the tweak cache and the pubshares are fixed key
material settled at key generation, never per-session parameters taken
from a coordinator or a peer; under that discipline a repeated call is
byte-identical and harmless, which is the point of a deterministic
nonce.

Worth raising against the BIP: the spec could close this by hashing the
untweaked threshold public key, at the cost of new test vectors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-01 23:37:22 +02:00
parent 34fa8e0b2e
commit 3765a82886
2 changed files with 42 additions and 0 deletions

View File

@@ -520,6 +520,25 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_frost_sign(
* through NonceAgg as a pubnonce contribution, and a pubnonce's components
* are never the point at infinity); if it does, this function fails.
*
* WARNING: the derivation above is the whole of what the nonce depends on. It
* does NOT commit to the pubshares, to the untweaked threshold public key, or
* to which tweaks the cache accumulated -- only to the x-only encoding of the
* _tweaked_ threshold public key (this is BIP 445's det_nonce_hash, not a
* deviation). Two tweak caches can therefore agree on that x-only key and
* still disagree on the sign g*gacc that multiplies the secret share, because
* Q and -Q have the same x-coordinate: a cache initialized from the threshold
* public key and one initialized from its negation are the simplest example.
* Two calls that differ only in that way emit the SAME pubnonce and two
* partial signatures that differ only in the sign of the secret-share term,
* which is two equations in the nonce and the secret share -- the secret
* share falls out of the pair.
*
* The caller must therefore treat the tweak cache and the pubshares as fixed
* key material belonging to the group, established once at key generation,
* and never as per-session parameters accepted from a coordinator or any
* other peer. Given that, repeating a call reproduces a byte-identical result
* and is harmless, which is the point of a deterministic nonce.
*
* Returns: 0 if the arguments are invalid or signing fails, 1 otherwise
* Args: ctx: pointer to a context object
* Out: partial_sig: pointer to a partial_sig object

View File

@@ -96,6 +96,29 @@ Security notes
unique for every call to `secp256k1_frost_nonce_gen`. Passing the secret
share to `nonce_gen` is recommended as defense-in-depth against bad
randomness.
- `secp256k1_frost_deterministic_sign` has no `session_secrand32` to keep
fresh; its safety rests instead on what the nonce derivation commits to. Per
BIP 445's `det_nonce_hash` that is the secret share, `my_id`, `u`, the sorted
ids, the aggothernonce, the **x-only** tweaked threshold public key, and the
message — and nothing else. In particular it does not commit to the
pubshares, to the untweaked threshold public key, or to the accumulated
tweaks. Since `Q` and `-Q` share an x-coordinate, a tweak cache initialized
from the threshold public key and one initialized from its negation present
the same x-only key to the derivation while disagreeing on the sign `g*gacc`
that multiplies the secret share. Two calls differing only in that produce
the same pubnonce and partial signatures `s = k + e*lambda*d` and
`s' = k - e*lambda*d`, where `k` is the identical `k1 + b*k2`; subtracting
them yields `d` directly. The same holds for any two caches that agree on
the tweaked x-only key but not on `g*gacc`.
This is a property of the specified derivation, not of this implementation,
and it is not detectable from inside a single call: the self-verification in
`Sign` passes in both cases, because each signature is individually valid
under its own cache. The caller carries the obligation. Treat the tweak
cache and the pubshares as fixed key material established once at key
generation, and never accept either as a per-session parameter from the
coordinator or another peer. Under that discipline a repeated call is
byte-identical and harmless, which is what the deterministic nonce is for.
- Final signatures produced by `secp256k1_frost_partial_sig_agg` are ordinary
BIP340 signatures; they are verified with `secp256k1_schnorrsig_verify`
against the (tweaked) x-only threshold public key.