mkit-attest 0.5.0

DSSE + in-toto v1 attestations for mkit, with multi-algorithm signers (Ed25519, secp256k1, P-256) and an RFC 8785 JCS encoder
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
//! BLS12-381 threshold signer — partial signing + aggregation.
//!
//! Part of the release-party flow (see `docs/specs/SPEC-RELEASE-THRESHOLD.md`).
//! This module exposes:
//!
//! * [`ThresholdSigner`] — a per-holder [`crate::Signer`] adapter that
//!   accepts a single trusted-dealer share + the group's aggregated
//!   public key and, on `sign`, returns a wire-encoded
//!   [`PartialSignature`].
//! * [`aggregate`] — a free function that recovers a threshold signature
//!   from M-of-N wire-encoded partials.
//! * [`verify`] — verify an already-aggregated threshold signature against
//!   the group's aggregated public key.
//!
//! The signer is currently **trusted-dealer-style**: a single party
//! runs `commonware-cryptography::bls12381::dkg::feldman_desmedt::deal_anonymous` and
//! hands each holder their share. A future revision may replace the
//! dealer with a proper DKG ceremony; the signer adapter is API-stable
//! because the dealer/DKG choice only affects how shares are produced,
//! not how they're consumed at sign time.
//!
//! ## Variant
//!
//! We pin the [`MinSig`] variant: signature in G1 (48 bytes), public
//! key in G2 (96 bytes). A "release party" produces one signature per
//! release that ships in the SLSA bundle, so we minimise the
//! signature side; the public key is registered once in the verifier
//! trust root and amortised across every release. This matches the
//! convention in the upstream threshold examples.
//!
//! ## Namespace
//!
//! BLS signing in commonware takes a namespace separate from the
//! message body; the on-curve hash is `union_unique(namespace, message)`
//! with the BLS DST. We use [`NAMESPACE`] = `b"mkit-attest/dsse/v1"`
//! so a release-party signature can't be replayed against an arbitrary
//! BLS verifier that happens to share the maintainer key.

use commonware_codec::{DecodeExt, Encode as _};
use commonware_cryptography::bls12381::dkg;
use commonware_cryptography::bls12381::primitives::group::Share;
use commonware_cryptography::bls12381::primitives::ops::{self, threshold};
use commonware_cryptography::bls12381::primitives::sharing::{Mode, Sharing};
use commonware_cryptography::bls12381::primitives::variant::{MinSig, PartialSignature, Variant};
use commonware_parallel::Sequential;
use commonware_utils::{Faults, N3f1};

use crate::Error;
use crate::algorithm::Algorithm;
use crate::signer::Signer;

/// DSSE namespace for mkit threshold signatures.
///
/// BLS signatures are domain-separated by hash-to-curve DST plus a
/// caller namespace. The DST is fixed by the curve (`MinSig` =
/// `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_`); the namespace below
/// stops a maintainer-set BLS key from being abused to sign arbitrary
/// messages outside the attestation flow. v1 is fixed for the duration
/// of Protocol v1; a future Protocol v2 bumps it.
pub const NAMESPACE: &[u8] = b"mkit-attest/dsse/v1";

/// Pinned variant. See module docs.
pub type V = MinSig;

/// Wire size of a `MinSig` aggregated signature (G1 compressed).
const SIGNATURE_SIZE: usize = 48;

/// Wire size of a `MinSig` group public key (G2 compressed).
pub const PUBLIC_KEY_SIZE: usize = 96;

/// Keyid prefix for BLS12-381 threshold-aggregated keys. The body is
/// the hex-encoded G2 compressed public key (192 hex chars), so the
/// full keyid is 13 + 192 = 205 chars.
pub const KEYID_PREFIX: &str = "bls12381-thr:";

/// A holder's threshold-signing adapter.
///
/// One [`ThresholdSigner`] per share. The aggregator never holds an
/// instance — it only sees serialised partial signatures coming back
/// from holders. The `sign` impl returns the partial signature
/// serialised via [`commonware_codec::Encode`] (so it can travel as
/// raw bytes through DSSE / a coordination channel) — call
/// [`aggregate`] to recover the threshold signature from M-of-N
/// partials.
#[derive(Debug)]
pub struct ThresholdSigner {
    /// This holder's private share (index + scalar).
    share: Share,
    /// The whole-cohort `Sharing` (public polynomial, mode, total).
    /// Held by every holder; it's the public-side material the dealer
    /// hands out alongside the share.
    sharing: Sharing<V>,
    /// Cached keyid (`bls12381-thr:<hex of compressed G2 pubkey>`).
    /// Computed once at construction so `keyid()` is allocation-free
    /// on the hot path.
    keyid: String,
}

impl ThresholdSigner {
    /// Construct a holder-side signer from a trusted-dealer share +
    /// the cohort's public sharing.
    ///
    /// The dealer is expected to have produced `(sharing, shares)` via
    /// [`commonware_cryptography::bls12381::dkg::feldman_desmedt::deal_anonymous`]
    /// (current trusted-dealer mode) or a future DKG protocol. The dealer
    /// hands the same `sharing` to every holder, plus one `Share` per
    /// holder.
    #[must_use]
    pub fn new(share: Share, sharing: Sharing<V>) -> Self {
        let pk_hex = mkit_core::to_hex_bytes(&sharing.public().encode());
        let keyid = format!("{KEYID_PREFIX}{pk_hex}");
        Self {
            share,
            sharing,
            keyid,
        }
    }

    /// Return the cohort's aggregated G2 public key (96 bytes,
    /// compressed — [`PUBLIC_KEY_SIZE`]). Verifier consumers register
    /// this under the keyid returned by [`Self::keyid`].
    #[must_use]
    pub fn aggregate_public_key(&self) -> Vec<u8> {
        self.sharing.public().encode().to_vec()
    }
}

impl Signer for ThresholdSigner {
    fn algorithm(&self) -> Algorithm {
        Algorithm::Bls12381Threshold
    }

    fn keyid(&self) -> Result<String, Error> {
        Ok(self.keyid.clone())
    }

    fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, Error> {
        let partial = threshold::sign_message::<V>(&self.share, NAMESPACE, pae);
        Ok(partial.encode().to_vec())
    }
}

/// Aggregate M-of-N wire-encoded partial signatures into a single
/// threshold signature.
///
/// `sharing` is the cohort's public polynomial — the same one held by
/// every signer. Uses the [`N3f1`] fault model (threshold =
/// quorum, i.e. ceil(2n/3) — matches the upstream tests).
///
/// # Errors
/// * [`Error::BlsThresholdPartialDecode`] — one or more `partials`
///   entries are not a valid wire-encoded [`PartialSignature`].
/// * [`Error::BlsThresholdInsufficientPartials`] — fewer than
///   `threshold` distinct partials were supplied, or the recovery
///   failed (e.g. duplicate indices).
pub fn aggregate(sharing: &Sharing<V>, partials: &[Vec<u8>]) -> Result<Vec<u8>, Error> {
    // Decode each partial. Any malformed entry kills the aggregate —
    // we deliberately don't best-effort, because the aggregator is
    // expected to have already verified each partial individually
    // (the future release-party CLI does this) and a bad partial
    // at aggregate time is a protocol violation, not a transient.
    let mut decoded: Vec<PartialSignature<V>> = Vec::with_capacity(partials.len());
    for p in partials {
        let ps = PartialSignature::<V>::decode(p.as_slice())
            .map_err(|_| Error::BlsThresholdPartialDecode)?;
        decoded.push(ps);
    }

    let sig = threshold::recover::<V, _>(sharing, &decoded, &Sequential)
        .map_err(|_| Error::BlsThresholdInsufficientPartials)?;

    Ok(sig.encode().to_vec())
}

/// Verify an aggregated threshold signature against the cohort's
/// group public key.
///
/// `aggregate_pubkey` is the G2 compressed public key returned by
/// [`ThresholdSigner::aggregate_public_key`] (96 bytes). `signature`
/// is a G1 compressed encoding (48 bytes) in the pinned `MinSig`
/// variant. `message` is the same PAE the holders signed.
///
/// # Errors
/// * [`Error::BlsThresholdPublicKeyDecode`] — `aggregate_pubkey` is
///   not a valid G2 compressed encoding (wrong length, off-curve, etc.).
/// * [`Error::BlsThresholdSignatureDecode`] — `signature` is not a
///   valid G1 compressed encoding (wrong length, off-curve, etc.).
/// * [`Error::BlsThresholdVerifyFailed`] — the signature is well-
///   formed but does not verify against `aggregate_pubkey`.
pub fn verify(aggregate_pubkey: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Error> {
    if aggregate_pubkey.len() != PUBLIC_KEY_SIZE {
        return Err(Error::BlsThresholdPublicKeyDecode);
    }
    let pk = <V as Variant>::Public::decode(aggregate_pubkey)
        .map_err(|_| Error::BlsThresholdPublicKeyDecode)?;
    if signature.len() != SIGNATURE_SIZE {
        return Err(Error::BlsThresholdSignatureDecode);
    }
    let sig = <V as Variant>::Signature::decode(signature)
        .map_err(|_| Error::BlsThresholdSignatureDecode)?;
    ops::verify_message::<V>(&pk, NAMESPACE, message, &sig)
        .map_err(|_| Error::BlsThresholdVerifyFailed)
}

/// Trusted-dealer helper: build a cohort of `n` holders with
/// the [`N3f1`] threshold (quorum = ceil(2n/3) per the fault model).
///
/// This is the dealer side of the trusted-dealer ceremony. A future
/// revision may replace it with a DKG protocol; the holder-side API
/// ([`ThresholdSigner`], [`aggregate`], [`verify`]) is stable across
/// that swap. Exposed for tests and a future release-party dealer
/// binary.
#[must_use]
pub fn trusted_dealer<R: rand_core::CryptoRng>(
    rng: &mut R,
    n: core::num::NonZeroU32,
) -> (Sharing<V>, Vec<Share>) {
    // `Mode` lost its `Default` impl in commonware 2026.9.0 (its
    // former `#[default]` was exactly this variant) — name it
    // explicitly so the derivation path stays pinned; see
    // `trusted_dealer_is_deterministic_for_a_seeded_rng` below, which
    // golden-vectors this exact call.
    dkg::feldman_desmedt::deal_anonymous::<V, N3f1>(rng, Mode::NonZeroCounter, n)
}

/// N3f1-quorum threshold for `n` holders under the N3f1 fault model.
/// Helper for tests and the future release-party CLI.
#[must_use]
pub fn threshold_for(n: u32) -> u32 {
    N3f1::quorum(n)
}

// Hex encoding goes through the workspace-canonical
// `mkit_core::hash::to_hex_bytes`; nothing BLS-specific here.

#[cfg(test)]
mod tests {
    use super::*;
    use commonware_utils::{NZU32, TestRng};

    /// Helper: deal a 3-of-? cohort. The N3f1 fault model with n=3
    /// gives quorum = 3 (no faults tolerated at n=3), so we use n=4
    /// to get a 3-of-4 cohort for the threshold tests. With n=4,
    /// `max_faults` = 1, quorum = 3.
    fn deal_3_of_4() -> (Sharing<V>, Vec<Share>) {
        let mut rng = TestRng::new(0x4242);
        trusted_dealer(&mut rng, NZU32!(4))
    }

    /// #505 PR 5/5: split from the former `roundtrip_3_of_4_aggregates_and_verifies`
    /// mega-test. Every holder's `keyid()` contract (algorithm, prefix,
    /// exact length, and — the whole point of threshold signing — every
    /// holder reporting the SAME keyid) is asserted here in isolation, so
    /// a keyid-contract regression can't mask (or be masked by) a failure
    /// in the sign/aggregate/verify crypto path exercised by
    /// `threshold_3_of_4_aggregate_recovers_verifying_signature` below.
    #[test]
    fn threshold_3_of_4_keyids_share_one_public_identity() {
        let (sharing, shares) = deal_3_of_4();
        assert_eq!(threshold_for(4), 3);

        let expected_kid = format!(
            "{KEYID_PREFIX}{}",
            mkit_core::to_hex_bytes(&sharing.public().encode())
        );
        for s in shares.iter().take(3) {
            let signer = ThresholdSigner::new(s.clone(), sharing.clone());
            assert_eq!(signer.algorithm(), Algorithm::Bls12381Threshold);
            let kid = signer.keyid().expect("keyid");
            assert!(kid.starts_with(KEYID_PREFIX));
            assert_eq!(kid.len(), KEYID_PREFIX.len() + PUBLIC_KEY_SIZE * 2);
            // Every holder reports the SAME keyid — that's the whole
            // point of threshold signing: one public identity.
            assert_eq!(kid, expected_kid);
        }
    }

    /// #505 PR 5/5: split from the former `roundtrip_3_of_4_aggregates_and_verifies`
    /// mega-test — the crypto path in isolation, with no interspersed
    /// keyid assertions to mask it: 3 of 4 holders sign a PAE; aggregate;
    /// the recovered threshold signature verifies against the cohort
    /// public key.
    #[test]
    fn threshold_3_of_4_aggregate_recovers_verifying_signature() {
        let (sharing, shares) = deal_3_of_4();

        // PAE the holders sign. The actual DSSE PAE shape doesn't
        // matter to BLS — it treats it as opaque bytes — but pinning
        // it to a representative DSSE PAE prefix is a regression
        // guard if the namespace ever moves.
        let pae = b"DSSEv1 28 application/vnd.in-toto+json 12 release v0.2.0";

        // Each of the first 3 holders produces a partial.
        let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
        for s in shares.iter().take(3) {
            let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
            let bytes = signer.sign(pae).expect("sign partial");
            partials_bytes.push(bytes);
        }

        // Aggregator combines partials. Aggregated signature is the
        // 48-byte G1 compressed form.
        let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");
        assert_eq!(agg_sig.len(), SIGNATURE_SIZE);

        // Verify against the group public key.
        let pk = sharing.public().encode().to_vec();
        assert_eq!(pk.len(), PUBLIC_KEY_SIZE);
        verify(&pk, pae, &agg_sig).expect("aggregated signature verifies");
    }

    /// `verify`'s length gates run before any decode/pairing work is
    /// attempted (SPEC-RELEASE-THRESHOLD §4). Feed a wrong-length
    /// aggregate signature (not `SIGNATURE_SIZE` = 48 bytes) directly
    /// and confirm it is rejected as a decode failure, never reaching
    /// pairing — a well-formed public key alone is enough to observe
    /// this, since the signature-length check runs unconditionally.
    #[test]
    fn verify_rejects_wrong_length_signature_before_pairing() {
        let (sharing, _shares) = deal_3_of_4();
        let pk = sharing.public().encode().to_vec();
        let pae = b"DSSEv1 4 test 2 hi";

        for len in [0, 1, SIGNATURE_SIZE - 1, SIGNATURE_SIZE + 1, 128] {
            let bogus_sig = vec![0u8; len];
            assert!(
                matches!(
                    verify(&pk, pae, &bogus_sig),
                    Err(Error::BlsThresholdSignatureDecode)
                ),
                "signature length {len} (!= {SIGNATURE_SIZE}) must be rejected before pairing"
            );
        }
    }

    /// Same gate on the other operand: a wrong-length cohort public
    /// key (not `PUBLIC_KEY_SIZE` = 96 bytes) must be rejected before
    /// any pairing work — even paired with an otherwise well-formed
    /// signature.
    #[test]
    fn verify_rejects_wrong_length_cohort_key_before_pairing() {
        let (sharing, shares) = deal_3_of_4();
        let pae = b"DSSEv1 4 test 2 hi";
        let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
        for s in shares.iter().take(3) {
            let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
            partials_bytes.push(signer.sign(pae).expect("partial"));
        }
        let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");

        for len in [0, 1, PUBLIC_KEY_SIZE - 1, PUBLIC_KEY_SIZE + 1, 256] {
            let bogus_pk = vec![0u8; len];
            assert!(
                matches!(
                    verify(&bogus_pk, pae, &agg_sig),
                    Err(Error::BlsThresholdPublicKeyDecode)
                ),
                "public key length {len} (!= {PUBLIC_KEY_SIZE}) must be rejected before pairing"
            );
        }
    }

    /// The DSSE namespace literal used for domain separation appears
    /// once in code and is shared by `ThresholdSigner::sign` and
    /// `verify`, so an accidental edit to one call site (but not the
    /// other) would re-verify against itself internally and pass every
    /// other test in this module. Pin the exact bytes directly.
    #[test]
    fn namespace_literal_is_pinned() {
        assert_eq!(NAMESPACE, b"mkit-attest/dsse/v1");
    }

    /// Insufficient threshold (1 of 4) returns
    /// `BlsThresholdInsufficientPartials` rather than producing a
    /// signature that would fail verify downstream.
    #[test]
    fn insufficient_threshold_returns_error() {
        let (sharing, shares) = deal_3_of_4();
        let pae = b"DSSEv1 4 test 2 hi";

        // One partial — below threshold of 3.
        let mut signer = ThresholdSigner::new(shares[0].clone(), sharing.clone());
        let only = signer.sign(pae).expect("partial");
        let partials = vec![only];

        match aggregate(&sharing, &partials) {
            Err(Error::BlsThresholdInsufficientPartials) => {}
            other => panic!("expected BlsThresholdInsufficientPartials, got {other:?}"),
        }
    }

    /// Two of three insufficient: with n=4 the quorum is 3, so 2
    /// partials must also fail.
    #[test]
    fn two_of_four_also_insufficient() {
        let (sharing, shares) = deal_3_of_4();
        let pae = b"DSSEv1 4 test 2 hi";

        let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(2);
        for s in shares.iter().take(2) {
            let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
            partials_bytes.push(signer.sign(pae).expect("partial"));
        }

        assert!(matches!(
            aggregate(&sharing, &partials_bytes),
            Err(Error::BlsThresholdInsufficientPartials)
        ));
    }

    /// Tamper: flip a bit inside one partial. Decoding may succeed
    /// (the bytes still decode as a G2 point) but the recovered
    /// aggregate must fail verify. We assert verify fails — the
    /// crypto semantics are what matter.
    #[test]
    fn tampered_partial_fails_aggregate_verify() {
        let (sharing, shares) = deal_3_of_4();
        let pae = b"DSSEv1 28 application/vnd.in-toto+json 2 {}";

        let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
        for s in shares.iter().take(3) {
            let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
            partials_bytes.push(signer.sign(pae).expect("partial"));
        }

        // Flip a bit in the *value* half of partial 1 (skip the
        // Participant index prefix at the front).
        //
        // PartialSignature wire form: <index><signature_bytes>. The
        // index is a small fixed-size prefix (4 bytes); we mutate
        // well past it so we're guaranteed to hit the signature.
        let last = partials_bytes[1].len() - 1;
        partials_bytes[1][last] ^= 0x01;

        // Either: decode fails outright (bad encoding) — that's
        // BlsThresholdPartialDecode, which is also a valid tamper
        // outcome. OR: decode succeeds but verify fails. Both are
        // acceptable rejections; what we care about is that the
        // tampered aggregate is NOT accepted.
        match aggregate(&sharing, &partials_bytes) {
            Err(Error::BlsThresholdPartialDecode) => {
                // Decoder caught the tamper. Done.
            }
            Ok(agg_sig) => {
                let pk = sharing.public().encode().to_vec();
                assert!(
                    matches!(
                        verify(&pk, pae, &agg_sig),
                        Err(Error::BlsThresholdVerifyFailed)
                    ),
                    "tampered partial must not produce a verifying aggregate"
                );
            }
            other => panic!("unexpected aggregate result on tampered partial: {other:?}"),
        }
    }

    /// Tamper on the message: a signature aggregated over `pae_signed`
    /// must not verify against a different `pae_other`. Crypto sanity
    /// — paired with the tampered-partial test above to cover both
    /// sides.
    #[test]
    fn aggregated_signature_does_not_verify_wrong_message() {
        let (sharing, shares) = deal_3_of_4();
        let pae_signed = b"DSSEv1 1 a 1 b";
        let pae_other = b"DSSEv1 1 a 1 c";

        let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
        for s in shares.iter().take(3) {
            let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
            partials_bytes.push(signer.sign(pae_signed).expect("partial"));
        }
        let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");
        let pk = sharing.public().encode().to_vec();

        verify(&pk, pae_signed, &agg_sig).expect("signed message verifies");
        match verify(&pk, pae_other, &agg_sig) {
            Err(Error::BlsThresholdVerifyFailed) => {}
            other => panic!("expected verify failure on wrong message, got {other:?}"),
        }
    }

    /// Algorithm enum integration: build a `SignerFrame` carrying
    /// `algorithm = ALGORITHM_BLS12381_THRESHOLD` and round-trip it
    /// through the buffa codegen — the variant survives.
    ///
    /// This guards against the proto enum integer drifting (5) and
    /// confirms the new variant is wired through `mkit-rpc`'s codegen.
    #[test]
    fn algorithm_enum_round_trips_through_buffa() {
        use buffa::Message;
        use mkit_rpc::mkit::rpc::v1::Algorithm as RpcAlgorithm;
        use mkit_rpc::mkit::rpc::v1::signer::{SignRequest, SignerFrame, signer_frame};

        let frame = SignerFrame {
            body: Some(signer_frame::Body::SignRequest(Box::new(
                SignRequest::default()
                    .with_algorithm(RpcAlgorithm::Bls12381Threshold)
                    .with_payload(b"DSSEv1 1 a 1 b".to_vec()),
            ))),
            ..Default::default()
        };

        let bytes = frame.encode_to_vec();
        let decoded = SignerFrame::decode(&mut &bytes[..]).expect("decode");
        let Some(signer_frame::Body::SignRequest(req)) = decoded.body else {
            panic!("expected SignRequest body");
        };
        assert_eq!(req.algorithm, Some(RpcAlgorithm::Bls12381Threshold.into()),);
        // Pin the wire integer: this test fails if anyone renumbers
        // the enum.
        assert_eq!(RpcAlgorithm::Bls12381Threshold as i32, 5);
    }

    /// Golden vector: `trusted_dealer` (and therefore
    /// `dkg::feldman_desmedt::deal_anonymous`) must derive the exact
    /// same cohort public key for a fixed seeded RNG and cohort size
    /// across a commonware version bump. This pins the derivation
    /// path — including the `Mode` passed to `deal_anonymous` — so a
    /// future edit that changes `Mode::default()` to an explicit
    /// variant (or an upstream DKG change) that alters share/point
    /// derivation is caught here, rather than only showing up as a
    /// mismatched keyid between two mkit builds signing the same
    /// cohort.
    ///
    /// Captured on commonware `2026.7.1` (`Mode::default()` ==
    /// `Mode::NonZeroCounter`, `sharing.rs:28-32` upstream at that
    /// tag). Must stay byte-identical after the `2026.9.0` bump,
    /// where `Mode` lost its `Default` impl and callers must name the
    /// variant explicitly.
    #[test]
    fn trusted_dealer_is_deterministic_for_a_seeded_rng() {
        let mut rng = TestRng::new(0x1357_9bdf);
        let (sharing, shares) = trusted_dealer(&mut rng, NZU32!(4));

        assert_eq!(shares.len(), 4);
        assert_eq!(
            mkit_core::to_hex_bytes(&sharing.public().encode()),
            "8bdb735ad697430b7ff022196750e30b53c6b6596947c59c18c6f498410cd7\
b3e93f3080b7772033b12fe2a3f3d02b4b08a6b9827d9dcb53be24ff40f5b8e48\
4612fdc8fffa694cc8d4b5234715f58c8845dce9e29b3e5cb2fe1361bca819893"
        );
    }
}