Skip to main content

confium_signatif/
ceremony.rs

1//! Ceremony records (SIGNATIF §17).
2//!
3//! Every threshold ceremony — DKG, re-share, signing, revocation —
4//! produces a verifiable [`CeremonyTranscript`]: the participants with
5//! their contribution proofs, the quorum parameters, the canonical
6//! payload hash signed, the aggregate threshold signature produced,
7//! and a timestamp. Each participant signs the transcript attesting
8//! their participation. The [`ceremony_audit`] algorithm verifies
9//! every member signature, the aggregate signature, the T-of-N
10//! participation, the transparency-log cross-reference, and timestamp
11//! consistency.
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15
16use crate::error::{SignatifError, SignatifResult};
17use crate::graph::{Quorum, SignatureVerifier};
18
19/// One participant's contribution record.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Participant {
22    /// Participant identity (organization or member key fingerprint).
23    pub id: String,
24    /// The participant's public key.
25    pub public_key: Vec<u8>,
26    /// Proof of contribution — for DKG, the member's PoP over the
27    /// session transcript (rogue-key prevention, §10); for signing,
28    /// the member's partial-signature commitment.
29    pub contribution_proof: Vec<u8>,
30    /// The participant's signature over the transcript participation
31    /// binding bytes (`transcript-signing` requirement).
32    pub participation_signature: Vec<u8>,
33}
34
35/// A verifiable record of one threshold ceremony.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CeremonyTranscript {
38    /// Ceremony type (from the ceremony-type registry): dkg, reshare,
39    /// sign, revoke, rotation.
40    pub ceremony_type: String,
41    /// Participating members with contribution proofs.
42    pub participants: Vec<Participant>,
43    /// Quorum parameters of the authority that ran the ceremony.
44    pub quorum: Quorum,
45    /// The canonical payload hash the ceremony signed.
46    pub payload_hash: [u8; 32],
47    /// The aggregate threshold signature produced.
48    pub aggregate_signature: Vec<u8>,
49    /// The authority's aggregate public key (verifies the aggregate).
50    pub aggregate_key: Vec<u8>,
51    /// When the ceremony concluded.
52    pub timestamp: DateTime<Utc>,
53    /// Transparency-log sequence of the artifact/certificate the
54    /// ceremony produced (`transcript-log-cross-reference`).
55    pub log_sequence: u64,
56}
57
58impl CeremonyTranscript {
59    /// The bytes each participant signs to attest participation:
60    /// ceremony type, member identity, quorum, payload hash, timestamp.
61    pub fn participation_bytes(&self, member_id: &str) -> SignatifResult<Vec<u8>> {
62        let v = serde_json::json!({
63            "ceremony_type": self.ceremony_type,
64            "member": member_id,
65            "quorum": self.quorum,
66            "payload_hash": hex::encode(self.payload_hash),
67            "timestamp": self.timestamp.to_rfc3339(),
68        });
69        Ok(crate::jcs::canonicalize(&v)?.into_bytes())
70    }
71}
72
73/// The audit algorithm (`audit-algorithm` requirement): verify each
74/// member participation signature, verify the aggregate threshold
75/// signature against the published aggregate key, confirm at least T
76/// of N members participated, cross-reference the payload with the
77/// transparency log entry, and confirm timestamp consistency.
78///
79/// `log_entry_payload_hash` is the hash recovered from the referenced
80/// transparency-log sequence number.
81///
82/// # Errors
83///
84/// Returns [`SignatifError::Ceremony`] with a precise reason for every
85/// failed audit step.
86pub fn ceremony_audit(
87    transcript: &CeremonyTranscript,
88    verifier: &dyn SignatureVerifier,
89    log_entry_payload_hash: Option<&[u8; 32]>,
90) -> SignatifResult<()> {
91    // 1. Member participation signatures.
92    for p in &transcript.participants {
93        let bytes = transcript.participation_bytes(&p.id)?;
94        if !verifier.verify(&p.public_key, &bytes, &p.participation_signature) {
95            return Err(SignatifError::Ceremony(format!(
96                "participation signature of member {} failed",
97                p.id
98            )));
99        }
100    }
101
102    // 2. Quorum: at least T of N participated, N consistent.
103    if transcript.participants.len() < transcript.quorum.t as usize {
104        return Err(SignatifError::Ceremony(format!(
105            "quorum not met: {} of {} required, {} participated",
106            transcript.quorum.t,
107            transcript.quorum.n,
108            transcript.participants.len()
109        )));
110    }
111    if transcript.participants.len() > transcript.quorum.n as usize {
112        return Err(SignatifError::Ceremony(format!(
113            "more participants ({}) than committee size N ({})",
114            transcript.participants.len(),
115            transcript.quorum.n
116        )));
117    }
118
119    // 3. Aggregate threshold signature over the payload hash.
120    if !verifier.verify(
121        &transcript.aggregate_key,
122        &transcript.payload_hash,
123        &transcript.aggregate_signature,
124    ) {
125        return Err(SignatifError::Ceremony(
126            "aggregate threshold signature failed".into(),
127        ));
128    }
129
130    // 4. Transparency-log cross-reference.
131    match log_entry_payload_hash {
132        Some(h) if *h != transcript.payload_hash => {
133            return Err(SignatifError::Ceremony(
134                "transparency log entry payload hash mismatch".into(),
135            ));
136        }
137        None => {
138            return Err(SignatifError::Ceremony(
139                "transparency log entry not available".into(),
140            ));
141        }
142        _ => {}
143    }
144
145    // 5. Timestamp consistency: not in the future.
146    if transcript.timestamp > Utc::now() {
147        return Err(SignatifError::Ceremony(
148            "transcript timestamp is in the future".into(),
149        ));
150    }
151    Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use ed25519_dalek::Signer;
158
159    fn generate_key() -> ed25519_dalek::SigningKey {
160        use rand_core::RngCore;
161        let mut seed = [0u8; 32];
162        rand_core::OsRng.fill_bytes(&mut seed);
163        ed25519_dalek::SigningKey::from_bytes(&seed)
164    }
165
166    struct Ed25519Verifier;
167
168    impl SignatureVerifier for Ed25519Verifier {
169        fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
170            use ed25519_dalek::Signature;
171            use ed25519_dalek::Verifier;
172            let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
173                return false;
174            };
175            let Ok(signature) = Signature::from_slice(sig) else {
176                return false;
177            };
178            vk.verify(msg, &signature).is_ok()
179        }
180    }
181
182    fn transcript(t: u32, n: u32, participating: usize) -> (CeremonyTranscript, [u8; 32]) {
183        let payload_hash = [7u8; 32];
184        let timestamp = Utc::now();
185        let mut participants = Vec::new();
186        for i in 0..participating {
187            let sk = generate_key();
188            let template = CeremonyTranscript {
189                ceremony_type: "dkg".into(),
190                participants: vec![],
191                quorum: Quorum { t, n },
192                payload_hash,
193                aggregate_signature: vec![],
194                aggregate_key: vec![],
195                timestamp,
196                log_sequence: 99,
197            };
198            let bytes = template.participation_bytes(&format!("m{i}")).unwrap();
199            participants.push(Participant {
200                id: format!("m{i}"),
201                public_key: sk.verifying_key().as_bytes().to_vec(),
202                contribution_proof: sk.sign(&bytes).to_bytes().to_vec(),
203                participation_signature: sk.sign(&bytes).to_bytes().to_vec(),
204            });
205        }
206        let agg_sk = generate_key();
207        let transcript = CeremonyTranscript {
208            ceremony_type: "dkg".into(),
209            participants,
210            quorum: Quorum { t, n },
211            payload_hash,
212            aggregate_signature: agg_sk.sign(&payload_hash).to_bytes().to_vec(),
213            aggregate_key: agg_sk.verifying_key().as_bytes().to_vec(),
214            timestamp,
215            log_sequence: 99,
216        };
217        (transcript, payload_hash)
218    }
219
220    #[test]
221    fn audit_passes_for_valid_transcript() {
222        let (t, hash) = transcript(2, 3, 3);
223        // Re-sign participation signatures correctly (helper signed
224        // participation with same key already).
225        assert!(ceremony_audit(&t, &Ed25519Verifier, Some(&hash)).is_ok());
226    }
227
228    #[test]
229    fn audit_fails_below_quorum() {
230        let (t, hash) = transcript(2, 3, 1);
231        assert!(ceremony_audit(&t, &Ed25519Verifier, Some(&hash)).is_err());
232    }
233
234    #[test]
235    fn audit_fails_on_log_mismatch() {
236        let (t, _) = transcript(2, 3, 3);
237        let wrong = [0u8; 32];
238        let err = ceremony_audit(&t, &Ed25519Verifier, Some(&wrong)).unwrap_err();
239        assert!(err.to_string().contains("log entry"));
240    }
241
242    #[test]
243    fn audit_fails_on_bad_aggregate() {
244        let (mut t, hash) = transcript(2, 3, 3);
245        t.aggregate_signature[0] ^= 1;
246        assert!(ceremony_audit(&t, &Ed25519Verifier, Some(&hash)).is_err());
247    }
248}