1use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15
16use crate::error::{SignatifError, SignatifResult};
17use crate::graph::{Quorum, SignatureVerifier};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Participant {
22 pub id: String,
24 pub public_key: Vec<u8>,
26 pub contribution_proof: Vec<u8>,
30 pub participation_signature: Vec<u8>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CeremonyTranscript {
38 pub ceremony_type: String,
41 pub participants: Vec<Participant>,
43 pub quorum: Quorum,
45 pub payload_hash: [u8; 32],
47 pub aggregate_signature: Vec<u8>,
49 pub aggregate_key: Vec<u8>,
51 pub timestamp: DateTime<Utc>,
53 pub log_sequence: u64,
56}
57
58impl CeremonyTranscript {
59 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
73pub fn ceremony_audit(
87 transcript: &CeremonyTranscript,
88 verifier: &dyn SignatureVerifier,
89 log_entry_payload_hash: Option<&[u8; 32]>,
90) -> SignatifResult<()> {
91 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 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 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 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 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 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}