Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tinycloud-threshold/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The crate is a workspace member with `publish = false`. No other workspace crate

The §3.6 request-digest amendment is explicit in SPEC.md. It makes an identity or transport-key transplant a context mismatch before share equations are evaluated.

`combine` accepts at most `2*n` raw share entries, where `n` is the validated current member count. Duplicates count toward this limit. An oversized input returns `CombineError::TooManyShares`, naming no member, before request/share binding checks or algebra. Within the limit, the existing binding-first checks and member deduplication remain unchanged.

## Checks

Use the workspace's pinned Rust 1.97.1 toolchain:
Expand Down
1 change: 1 addition & 0 deletions tinycloud-threshold/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ unwrap(k: &DerivedKey, expected_id, (U, V, W)):
- **Issue** (member i, `KeyShare` with context X): require the request's context fields equal X exactly, else `Error::ContextMismatch` (no share released). r_i ← Zr* (§3.2 sampling). Share = (i, context digest, request digest, c1 = r_i·g1, c2 = r_i·g2, c3 = s_i·H1(id) + r_i·tpk). The request digest is the approved S0.1 amendment recorded below.
- **Response signing preimage** = TH(`…DECRYPT-SHARE`, request digest ‖ certificate digest ‖ u16be(i) ‖ encode(share)). The node signs it with its node key.
- **Verify** against the `PublicKeySet` of context X: first the context check (share's and request's context = X; share's request digest = request digest; member i in X); a mismatch is `ContextMismatch`, never `InvalidShare`. Then e(c1, g2) = e(g1, c2) and e(c3, g2) = e(H1(id), pk_i)·e(tpk, c2), each checked as one multi-pairing; failure is `ShareError::Invalid { member }`. These equations follow the published aggregatable vetBLS scheme.
- **Combine input bound:** take n from the validated current `PublicKeySet` and count all raw share entries, including duplicates. Accept at most `2*n` entries. Above that bound, return `CombineError::TooManyShares`, naming no member, before request/share binding checks, list iteration, allocation proportional to the supplied entries or algebra. For accepted-length inputs, request mismatch remains `CombineError::ContextMismatch`; all share bindings, including surplus entries, are checked before algebra. The following context-first combine rule applies within this bound.
- **Combine:** context check on every share first (mixed contexts → `CombineError::MixedContexts`; this check is unconditional, independently of the final pairing result). Each share verified individually; duplicates by member counted once; needs ≥ t; uses the t lowest indices. C_j = Σ λ_i^S·c_{j,i}.
- **Decrypt key:** k = C3 − tsk·C1. Accept only if k ≠ identity and e(k, g2) = e(H1(id), mpk). Returns `DerivedKey { identity, key context digest, k }`. This final check checks the key against id and mpk; context and authorization checks run separately first.

Expand Down
91 changes: 91 additions & 0 deletions tinycloud-threshold/src/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,97 @@ fn signing_preimage_rejects_changed_record_nonce() {
);
}

#[test]
fn p3_combine_accepts_exact_raw_limit() {
let (public, _, tsk, req, issued) = setup(11);
let repeated: [EncryptedKeyShare; 6] =
std::array::from_fn(|i| issued[i % issued.len()].clone());
let aggregate = combine(&public, &req, &repeated).unwrap();
let key = decrypt_derived_key(&aggregate, &tsk, &public, &req).unwrap();
let data = DataKey::new([0x42; 32]);
let ciphertext = wrap_key(
public.master_public_key(),
req.identity(),
&data,
&mut ChaCha20Rng::from_seed([12; 32]),
)
.unwrap();
assert_eq!(
unwrap_key(&key, req.identity(), &ciphertext)
.unwrap()
.as_bytes(),
data.as_bytes()
);
}

#[test]
fn p3_combine_counts_duplicate_members_once_at_limit() {
let (public, _, _, req, issued) = setup(11);
let repeated: [EncryptedKeyShare; 6] = std::array::from_fn(|_| issued[0].clone());
assert!(matches!(
combine(&public, &req, &repeated),
Err(CombineError::NotEnoughShares)
));
}

#[test]
fn p3_combine_rejects_above_raw_limit() {
let (public, _, _, req, issued) = setup(11);
for share in &issued {
assert_eq!(verify_share(&public, &req, share), Ok(()));
}
let too_many: [EncryptedKeyShare; 7] =
std::array::from_fn(|i| issued[i % issued.len()].clone());
let result = combine(&public, &req, &too_many);
let maximum = 2 * usize::from(public.context().threshold().n());
assert!(
result.is_err(),
"combine accepted {} raw entries above 2*n={maximum}",
too_many.len()
);
assert_eq!(result.err(), Some(CombineError::TooManyShares));
}

#[test]
fn p3_combine_limit_precedes_context_and_share_checks() {
let (public, _, _, req, issued) = setup(11);
let mut too_many: [EncryptedKeyShare; 7] =
std::array::from_fn(|i| issued[i % issued.len()].clone());
let mut other_request = req.clone();
other_request.context_digest[0] ^= 1;
assert_eq!(
combine(&public, &other_request, &too_many).unwrap_err(),
CombineError::TooManyShares
);
too_many[6].context_digest[0] ^= 1;
assert_eq!(
combine(&public, &req, &too_many).unwrap_err(),
CombineError::TooManyShares
);
too_many[6].context_digest[0] ^= 1;
too_many[6].c3 = G1::identity();
assert_eq!(
combine(&public, &req, &too_many).unwrap_err(),
CombineError::TooManyShares
);
}

#[test]
fn p3_verify_share_rejects_canonical_identity_c3_mutation() {
let (public, _, _, req, issued) = setup(11);
assert_eq!(verify_share(&public, &req, &issued[0]), Ok(()));
assert!(!issued[0].c3.is_identity());
let mut changed = issued[0].clone();
changed.c3 = G1::identity();
let changed = EncryptedKeyShare::decode(&changed.encode()).unwrap();
assert_eq!(
verify_share(&public, &req, &changed),
Err(ShareError::Invalid {
member: issued[0].index()
})
);
}

#[test]
fn checkpoint_2of3() {
let (public, shares, tsk, req, issued) = setup(11);
Expand Down
6 changes: 6 additions & 0 deletions tinycloud-threshold/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,17 @@ pub struct EncryptedDerivedKey {
}

/// Verifies every input, counts each member once, then uses the lowest t indices.
/// Refuses more than `2 * n` entries, including duplicates, before context checks
/// or algebra, where `n` is the validated current member count.
pub fn combine(
public: &PublicKeySet,
req: &DecryptRequest,
shares: &[EncryptedKeyShare],
) -> Result<EncryptedDerivedKey, CombineError> {
let maximum = usize::from(public.context.threshold().n()).saturating_mul(2);
if shares.len() > maximum {
return Err(CombineError::TooManyShares);
}
if !req.matches(&public.context) {
return Err(CombineError::ContextMismatch);
}
Expand Down
3 changes: 3 additions & 0 deletions tinycloud-threshold/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub enum ShareError {
/// Combining never interprets mixed-context inputs as member faults.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CombineError {
/// Raw entries exceed twice the current member count; no member is blamed.
#[error("too many supplied share entries")]
TooManyShares,
#[error("request context mismatch")]
ContextMismatch,
#[error("mixed share contexts")]
Expand Down
Loading