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.