Threshold signing end-to-end
A worked example: generate a P-256 keypair, Shamir-split it across 5
parties with threshold 3, recover from any 3, and sign a message. The
complete Ruby equivalent lives at
examples/threshold_signing.rb;
the Rust outline below mirrors it.
Setup
cargo add confium-tc-frost-p256
Generate a keypair
use confium_tc_frost_p256::{generate_keypair, public_key_sec1};
let kp = generate_keypair();
// kp.secret_scalar: p256::Scalar (zeroized on drop)
// kp.public_key: p256::AffinePoint
println!("public key (SEC1): {}", hex::encode(public_key_sec1(&kp.public_key)));
Keypair is a plain struct with two fields — secret_scalar and
public_key. The secret is a p256::Scalar which zeroizes on drop
via elliptic_curve::SecretScope.
Split into 5 shares, threshold 3
use confium_tc_frost_p256::split_secret;
let shares = split_secret(&kp.secret_scalar, 3, 5);
println!("split into {} shares, threshold = 3", shares.len());
for (i, s) in shares.iter().enumerate() {
// s.x is the party index (1-based); s.y is the share value.
println!(" share[{}]: x={}", i, s.x);
}
Recover from any 3 of the 5
use confium_tc_frost_p256::recover_secret;
let subset: Vec<&_> = vec![&shares[0], &shares[2], &shares[4]];
let recovered = recover_secret(&subset).expect("recover");
assert_eq!(recovered, kp.secret_scalar);
println!("recovered from shares [0, 2, 4]: MATCH");
Insufficient shares return the wrong answer
With only 2 shares (below the threshold), recover_secret returns a
valid P-256 scalar that is not the original secret. This is the
mathematical nature of Shamir: any T−1 shares interpolate to a
different polynomial that passes through (0, secret') for some
random-looking secret'.
let subset: Vec<&_> = vec![&shares[0], &shares[1]];
let wrong = recover_secret(&subset).expect("interpolates to something");
assert_ne!(wrong, kp.secret_scalar);
println!("recovered from shares [0, 1]: WRONG (expected)");
The recovery API does not — and cannot — distinguish “right” from “wrong” without comparing to the original secret. Callers must enforce the threshold in the orchestrating logic.
Sign with the original key
use confium_tc_frost_p256::sign_message;
let signed = sign_message(&kp, b"threshold test").expect("sign");
// signed.der_bytes: DER-encoded per RFC 3279 (what X.509 and TLS expect)
// signed.fixed_bytes: fixed-length 64-byte (r || s) (for compact protocols)
println!("der: {} bytes, fixed: {} bytes",
signed.der_bytes.len(), signed.fixed_bytes.len());
Any standard ECDSA-P256 verifier (OpenSSL, BoringSSL, Botan, the
p256 crate’s verify function) will accept this signature.
Multi-party signing
For real deployments, signers do not hold the full secret — they hold
individual shares. A signing session collects T signature shares and
aggregates them into the final signature without ever reconstructing
the secret. That orchestration is handled by
confium-coordinator.
Related
confium-tc-frost-p256deep dive.confium-coordinatorfor the multi-party session protocol.- Threshold crypto 101 on the public website.