1use std::collections::BTreeMap;
2
3use chio_core::canonical::canonical_json_bytes;
4use chio_core::{sha256_hex, StoreMutationFence};
5use chio_federation_authority::{
6 advance_frost_ceremony, begin_frost_ceremony, complete_frost_ceremony,
7 verify_frost_ceremony_round1_transcript, verify_frost_ceremony_transcript,
8 FrostAuthenticatedDkgPackage, FrostCeremonyConfig, FrostCeremonySecret,
9 FrostCeremonySecretKind,
10};
11use rand_core::{CryptoRng, RngCore};
12use rusqlite::{params, Connection, OptionalExtension, Row, Transaction};
13use serde::Serialize;
14use zeroize::Zeroizing;
15
16use super::commit::{
17 append_projection_commit, prefixed_digest, verify_projection_commit_chains, ProjectionMutation,
18};
19use super::{
20 secret_kind_name, FrostCeremonyRecord, FrostCeremonyRound1Record, FrostCeremonyRound2Record,
21 FrostCeremonyState, FrostCustodyKey, FrostStoreError, SqliteFrostStore, StoredCeremonyOutput,
22 StoredFrostCeremonyCompletion,
23};
24use crate::encrypted_blob::{decrypt_blob_with_aad, try_encrypt_blob_with_aad, EncryptedBlob};
25
26const CUSTODY_AAD_FORMAT: &str = "chio.frost.ceremony-custody-aad.v1";
27const RECORD_DIGEST_PREFIX: &[u8] = b"chio.frost.ceremony-record.digest.v1\0";
28
29#[derive(Debug)]
30struct StoredCeremonyRow {
31 ceremony_id: String,
32 config_json: Vec<u8>,
33 config_digest: String,
34 participant_set_digest: String,
35 scope_id: String,
36 key_epoch: u64,
37 local_participant_id: String,
38 state: FrostCeremonyState,
39 state_version: u64,
40 custody_generation: String,
41 secret_kind: String,
42 secret: EncryptedBlob,
43 output: Option<EncryptedBlob>,
44 input_transcript_digest: Option<String>,
45 public_key_package: Option<Vec<u8>>,
46 group_public_key: Option<String>,
47 verification_shares_json: Option<Vec<u8>>,
48 source_fence: StoreMutationFence,
49 updated_at_unix_ms: u64,
50 completed_at_unix_ms: Option<u64>,
51 record_digest: String,
52}
53
54struct CeremonyWrite<'a> {
55 ceremony_id: &'a str,
56 config_json: &'a [u8],
57 config_digest: &'a str,
58 participant_set_digest: &'a str,
59 scope_id: &'a str,
60 key_epoch: u64,
61 local_participant_id: &'a str,
62 state: FrostCeremonyState,
63 state_version: u64,
64 custody_generation: &'a str,
65 secret_kind: &'a str,
66 secret: &'a EncryptedBlob,
67 output: Option<&'a EncryptedBlob>,
68 input_transcript_digest: Option<&'a str>,
69 public_key_package: Option<&'a [u8]>,
70 group_public_key: Option<&'a str>,
71 verification_shares_json: Option<&'a [u8]>,
72 source_fence: &'a StoreMutationFence,
73 updated_at_unix_ms: u64,
74 completed_at_unix_ms: Option<u64>,
75}
76
77struct CustodyBinding<'a> {
78 ceremony_id: &'a str,
79 config_digest: &'a str,
80 state: FrostCeremonyState,
81 state_version: u64,
82 custody_generation: &'a str,
83 fence: &'a StoreMutationFence,
84 updated_at_unix_ms: u64,
85}
86
87impl StoredCeremonyRow {
88 fn custody_binding(&self) -> CustodyBinding<'_> {
89 CustodyBinding {
90 ceremony_id: &self.ceremony_id,
91 config_digest: &self.config_digest,
92 state: self.state,
93 state_version: self.state_version,
94 custody_generation: &self.custody_generation,
95 fence: &self.source_fence,
96 updated_at_unix_ms: self.updated_at_unix_ms,
97 }
98 }
99}
100
101#[derive(Serialize)]
102#[serde(rename_all = "camelCase")]
103struct CustodyAad<'a> {
104 format: &'static str,
105 purpose: &'a str,
106 ceremony_id: &'a str,
107 config_digest: &'a str,
108 state: FrostCeremonyState,
109 state_version: u64,
110 custody_generation: &'a str,
111 store_uuid: &'a str,
112 store_lease_id: &'a str,
113 store_owner_epoch: u64,
114 updated_at_unix_ms: u64,
115}
116
117#[derive(Serialize)]
118#[serde(rename_all = "camelCase")]
119struct RecordDigestPreimage<'a> {
120 format: &'static str,
121 ceremony_id: &'a str,
122 config_digest: &'a str,
123 participant_set_digest: &'a str,
124 scope_id: &'a str,
125 key_epoch: u64,
126 local_participant_id: &'a str,
127 state: FrostCeremonyState,
128 state_version: u64,
129 custody_generation: &'a str,
130 secret_kind: &'a str,
131 secret_nonce: String,
132 secret_ciphertext_digest: String,
133 output_nonce: Option<String>,
134 output_ciphertext_digest: Option<String>,
135 input_transcript_digest: Option<&'a str>,
136 public_key_package_digest: Option<String>,
137 group_public_key: Option<&'a str>,
138 verification_shares_digest: Option<String>,
139 store_uuid: &'a str,
140 store_lease_id: &'a str,
141 store_owner_epoch: u64,
142 updated_at_unix_ms: u64,
143 completed_at_unix_ms: Option<u64>,
144}
145
146impl SqliteFrostStore {
147 #[allow(clippy::too_many_arguments)]
148 pub fn begin_ceremony<R: CryptoRng + RngCore>(
149 &self,
150 config: &FrostCeremonyConfig,
151 transport_key: &chio_core::Keypair,
152 custody: &FrostCustodyKey,
153 rng: &mut R,
154 fence: &StoreMutationFence,
155 trusted_now_unix_ms: u64,
156 ) -> Result<FrostCeremonyRound1Record, FrostStoreError> {
157 validate_trusted_time(trusted_now_unix_ms)?;
158 let ceremony_id = config.ceremony_id()?;
159 let config_json = canonical_json_bytes(config)
160 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
161 let config_digest = sha256_hex(&config_json);
162 let participant_set_digest = config.participant_set_digest()?;
163
164 let mut connection = self.connection()?;
165 let transaction = self.begin_write(&mut connection, fence)?;
166 if let Some(stored) = load_ceremony_tx(&transaction, &ceremony_id)? {
167 verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
168 if stored.state != FrostCeremonyState::Round1Ready {
169 return Err(FrostStoreError::Conflict(
170 "ceremony already advanced beyond round one",
171 ));
172 }
173 let output = decrypt_output(&stored, custody)?;
174 let StoredCeremonyOutput::Round1(package) = output else {
175 return Err(invalid("round-one state retained another output kind"));
176 };
177 transaction.commit().map_err(super::sqlite_error)?;
178 return Ok(FrostCeremonyRound1Record {
179 ceremony_id,
180 state: stored.state,
181 state_version: stored.state_version,
182 package: *package,
183 });
184 }
185 let conflicting = transaction
186 .query_row(
187 r#"
188 SELECT ceremony_id FROM frost_ceremonies
189 WHERE scope_id = ?1 AND key_epoch = ?2 AND local_participant_id = ?3
190 "#,
191 params![
192 &config.scope_id,
193 sqlite_u64(config.key_epoch, "ceremony key epoch")?,
194 &config.local_participant_id,
195 ],
196 |row| row.get::<_, String>(0),
197 )
198 .optional()
199 .map_err(super::sqlite_error)?;
200 if conflicting.is_some() {
201 return Err(FrostStoreError::Conflict(
202 "another participant set already owns this scope and epoch",
203 ));
204 }
205
206 let transition = begin_frost_ceremony(config, transport_key, rng)?;
207 let state = FrostCeremonyState::Round1Ready;
208 let state_version = 1;
209 let custody_binding = CustodyBinding {
210 ceremony_id: &ceremony_id,
211 config_digest: &config_digest,
212 state,
213 state_version,
214 custody_generation: custody.generation(),
215 fence,
216 updated_at_unix_ms: trusted_now_unix_ms,
217 };
218 let secret = encrypt_material(
219 transition.secret.custody_bytes(),
220 custody,
221 &custody_aad("secret", &custody_binding)?,
222 )?;
223 let output_bytes = Zeroizing::new(
224 canonical_json_bytes(&StoredCeremonyOutput::Round1(Box::new(
225 transition.package.clone(),
226 )))
227 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?,
228 );
229 let output = encrypt_material(
230 &output_bytes,
231 custody,
232 &custody_aad("output", &custody_binding)?,
233 )?;
234 let write = CeremonyWrite {
235 ceremony_id: &ceremony_id,
236 config_json: &config_json,
237 config_digest: &config_digest,
238 participant_set_digest: &participant_set_digest,
239 scope_id: &config.scope_id,
240 key_epoch: config.key_epoch,
241 local_participant_id: &config.local_participant_id,
242 state,
243 state_version,
244 custody_generation: custody.generation(),
245 secret_kind: secret_kind_name(&transition.secret),
246 secret: &secret,
247 output: Some(&output),
248 input_transcript_digest: None,
249 public_key_package: None,
250 group_public_key: None,
251 verification_shares_json: None,
252 source_fence: fence,
253 updated_at_unix_ms: trusted_now_unix_ms,
254 completed_at_unix_ms: None,
255 };
256 let record_digest = record_digest(&write)?;
257 insert_ceremony(&transaction, &write, &record_digest)?;
258 append_projection_commit(
259 &transaction,
260 self,
261 &projection_key(&ceremony_id),
262 ProjectionMutation {
263 sequence: state_version,
264 projection_type: "ceremony",
265 mutation_kind: "frost.ceremony.begin",
266 record_digest: &record_digest,
267 },
268 fence,
269 )?;
270 self.commit_write(transaction)?;
271 self.sync_after_write(&connection)?;
272 Ok(FrostCeremonyRound1Record {
273 ceremony_id,
274 state,
275 state_version,
276 package: transition.package,
277 })
278 }
279
280 #[allow(clippy::too_many_arguments)]
281 pub fn advance_ceremony(
282 &self,
283 config: &FrostCeremonyConfig,
284 transport_key: &chio_core::Keypair,
285 custody: &FrostCustodyKey,
286 round1_packages: &[FrostAuthenticatedDkgPackage],
287 fence: &StoreMutationFence,
288 trusted_now_unix_ms: u64,
289 ) -> Result<FrostCeremonyRound2Record, FrostStoreError> {
290 validate_trusted_time(trusted_now_unix_ms)?;
291 let ceremony_id = config.ceremony_id()?;
292 let config_json = canonical_json_bytes(config)
293 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
294 let config_digest = sha256_hex(&config_json);
295 let round1_digest = verify_frost_ceremony_round1_transcript(config, round1_packages)?;
296
297 let mut connection = self.connection()?;
298 let transaction = self.begin_write(&mut connection, fence)?;
299 let stored = load_ceremony_tx(&transaction, &ceremony_id)?
300 .ok_or(FrostStoreError::Conflict("ceremony has not started"))?;
301 verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
302 if stored.state == FrostCeremonyState::Round2Ready {
303 if stored.input_transcript_digest.as_deref() != Some(&round1_digest) {
304 return Err(FrostStoreError::Conflict(
305 "round-one transcript changed after advancement",
306 ));
307 }
308 let output = decrypt_output(&stored, custody)?;
309 let StoredCeremonyOutput::Round2(packages) = output else {
310 return Err(invalid("round-two state retained another output kind"));
311 };
312 transaction.commit().map_err(super::sqlite_error)?;
313 return Ok(FrostCeremonyRound2Record {
314 ceremony_id,
315 state: stored.state,
316 state_version: stored.state_version,
317 packages,
318 round1_transcript_digest: round1_digest,
319 });
320 }
321 if stored.state != FrostCeremonyState::Round1Ready || stored.state_version != 1 {
322 return Err(FrostStoreError::Conflict(
323 "ceremony cannot advance from its current state",
324 ));
325 }
326 let round1_secret = decrypt_secret(&stored, custody)?;
327 let transition =
328 advance_frost_ceremony(config, transport_key, round1_secret, round1_packages)?;
329 if transition.round1_transcript_digest != round1_digest {
330 return Err(invalid(
331 "authority returned a different round-one transcript",
332 ));
333 }
334 let state = FrostCeremonyState::Round2Ready;
335 let state_version = 2;
336 let custody_binding = CustodyBinding {
337 ceremony_id: &ceremony_id,
338 config_digest: &config_digest,
339 state,
340 state_version,
341 custody_generation: custody.generation(),
342 fence,
343 updated_at_unix_ms: trusted_now_unix_ms,
344 };
345 let secret = encrypt_material(
346 transition.secret.custody_bytes(),
347 custody,
348 &custody_aad("secret", &custody_binding)?,
349 )?;
350 let output_bytes = Zeroizing::new(
351 canonical_json_bytes(&StoredCeremonyOutput::Round2(transition.packages.clone()))
352 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?,
353 );
354 let output = encrypt_material(
355 &output_bytes,
356 custody,
357 &custody_aad("output", &custody_binding)?,
358 )?;
359 let write = CeremonyWrite {
360 ceremony_id: &ceremony_id,
361 config_json: &config_json,
362 config_digest: &config_digest,
363 participant_set_digest: &stored.participant_set_digest,
364 scope_id: &stored.scope_id,
365 key_epoch: stored.key_epoch,
366 local_participant_id: &stored.local_participant_id,
367 state,
368 state_version,
369 custody_generation: custody.generation(),
370 secret_kind: secret_kind_name(&transition.secret),
371 secret: &secret,
372 output: Some(&output),
373 input_transcript_digest: Some(&round1_digest),
374 public_key_package: None,
375 group_public_key: None,
376 verification_shares_json: None,
377 source_fence: fence,
378 updated_at_unix_ms: trusted_now_unix_ms,
379 completed_at_unix_ms: None,
380 };
381 let record_digest = record_digest(&write)?;
382 update_ceremony(&transaction, &write, &record_digest, 1)?;
383 append_projection_commit(
384 &transaction,
385 self,
386 &projection_key(&ceremony_id),
387 ProjectionMutation {
388 sequence: state_version,
389 projection_type: "ceremony",
390 mutation_kind: "frost.ceremony.advance",
391 record_digest: &record_digest,
392 },
393 fence,
394 )?;
395 self.commit_write(transaction)?;
396 self.sync_after_write(&connection)?;
397 Ok(FrostCeremonyRound2Record {
398 ceremony_id,
399 state,
400 state_version,
401 packages: transition.packages,
402 round1_transcript_digest: round1_digest,
403 })
404 }
405
406 pub fn complete_ceremony(
407 &self,
408 config: &FrostCeremonyConfig,
409 custody: &FrostCustodyKey,
410 round1_packages: &[FrostAuthenticatedDkgPackage],
411 round2_packages: &[FrostAuthenticatedDkgPackage],
412 fence: &StoreMutationFence,
413 trusted_now_unix_ms: u64,
414 ) -> Result<StoredFrostCeremonyCompletion, FrostStoreError> {
415 validate_trusted_time(trusted_now_unix_ms)?;
416 let ceremony_id = config.ceremony_id()?;
417 let config_json = canonical_json_bytes(config)
418 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
419 let config_digest = sha256_hex(&config_json);
420 let transcript_digest =
421 verify_frost_ceremony_transcript(config, round1_packages, round2_packages)?;
422
423 let mut connection = self.connection()?;
424 let transaction = self.begin_write(&mut connection, fence)?;
425 let stored = load_ceremony_tx(&transaction, &ceremony_id)?
426 .ok_or(FrostStoreError::Conflict("ceremony has not started"))?;
427 verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
428 if stored.state == FrostCeremonyState::Completed {
429 if stored.input_transcript_digest.as_deref() != Some(&transcript_digest) {
430 return Err(FrostStoreError::Conflict(
431 "ceremony transcript changed after completion",
432 ));
433 }
434 let completion = completion_from_row(&stored)?;
435 transaction.commit().map_err(super::sqlite_error)?;
436 return Ok(completion);
437 }
438 if stored.state != FrostCeremonyState::Round2Ready || stored.state_version != 2 {
439 return Err(FrostStoreError::Conflict(
440 "ceremony cannot complete from its current state",
441 ));
442 }
443 let round2_secret = decrypt_secret(&stored, custody)?;
444 let completion =
445 complete_frost_ceremony(config, round2_secret, round1_packages, round2_packages)?;
446 if completion.transcript_digest != transcript_digest {
447 return Err(invalid(
448 "authority returned a different ceremony transcript",
449 ));
450 }
451 let state = FrostCeremonyState::Completed;
452 let state_version = 3;
453 let custody_binding = CustodyBinding {
454 ceremony_id: &ceremony_id,
455 config_digest: &config_digest,
456 state,
457 state_version,
458 custody_generation: custody.generation(),
459 fence,
460 updated_at_unix_ms: trusted_now_unix_ms,
461 };
462 let secret = encrypt_material(
463 completion.key_package.custody_bytes(),
464 custody,
465 &custody_aad("secret", &custody_binding)?,
466 )?;
467 let verification_shares_json = canonical_json_bytes(&completion.verification_shares)
468 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
469 let write = CeremonyWrite {
470 ceremony_id: &ceremony_id,
471 config_json: &config_json,
472 config_digest: &config_digest,
473 participant_set_digest: &stored.participant_set_digest,
474 scope_id: &stored.scope_id,
475 key_epoch: stored.key_epoch,
476 local_participant_id: &stored.local_participant_id,
477 state,
478 state_version,
479 custody_generation: custody.generation(),
480 secret_kind: secret_kind_name(&completion.key_package),
481 secret: &secret,
482 output: None,
483 input_transcript_digest: Some(&transcript_digest),
484 public_key_package: Some(&completion.public_key_package),
485 group_public_key: Some(&completion.group_public_key),
486 verification_shares_json: Some(&verification_shares_json),
487 source_fence: fence,
488 updated_at_unix_ms: trusted_now_unix_ms,
489 completed_at_unix_ms: Some(trusted_now_unix_ms),
490 };
491 let record_digest = record_digest(&write)?;
492 update_ceremony(&transaction, &write, &record_digest, 2)?;
493 append_projection_commit(
494 &transaction,
495 self,
496 &projection_key(&ceremony_id),
497 ProjectionMutation {
498 sequence: state_version,
499 projection_type: "ceremony",
500 mutation_kind: "frost.ceremony.complete",
501 record_digest: &record_digest,
502 },
503 fence,
504 )?;
505 self.commit_write(transaction)?;
506 self.sync_after_write(&connection)?;
507 Ok(StoredFrostCeremonyCompletion {
508 ceremony_id,
509 state,
510 state_version,
511 public_key_package: completion.public_key_package,
512 group_public_key: completion.group_public_key,
513 verification_shares: completion.verification_shares,
514 transcript_digest,
515 })
516 }
517
518 pub fn load_ceremony(
519 &self,
520 ceremony_id: &str,
521 custody: &FrostCustodyKey,
522 ) -> Result<Option<FrostCeremonyRecord>, FrostStoreError> {
523 let mut connection = self.connection()?;
524 let transaction = self.begin_read(&mut connection, None)?;
525 let Some(stored) = load_ceremony_tx(&transaction, ceremony_id)? else {
526 transaction.commit().map_err(super::sqlite_error)?;
527 return Ok(None);
528 };
529 drop(decrypt_secret(&stored, custody)?);
530 let record = public_record(&stored);
531 transaction.commit().map_err(super::sqlite_error)?;
532 Ok(Some(record))
533 }
534
535 pub fn load_completed_key_package(
536 &self,
537 ceremony_id: &str,
538 custody: &FrostCustodyKey,
539 fence: &StoreMutationFence,
540 ) -> Result<FrostCeremonySecret, FrostStoreError> {
541 let mut connection = self.connection()?;
542 let transaction = self.begin_read(&mut connection, Some(fence))?;
543 let stored = load_ceremony_tx(&transaction, ceremony_id)?
544 .ok_or(FrostStoreError::Conflict("ceremony is absent"))?;
545 if stored.state != FrostCeremonyState::Completed {
546 return Err(FrostStoreError::Conflict("ceremony is not complete"));
547 }
548 let secret = decrypt_secret(&stored, custody)?;
549 transaction.commit().map_err(super::sqlite_error)?;
550 Ok(secret)
551 }
552}
553
554pub(super) fn verify_ceremony_invariants(connection: &Connection) -> Result<(), FrostStoreError> {
555 let mut statement = connection
556 .prepare("SELECT ceremony_id FROM frost_ceremonies ORDER BY ceremony_id")
557 .map_err(super::sqlite_error)?;
558 let ids = statement
559 .query_map([], |row| row.get::<_, String>(0))
560 .map_err(super::sqlite_error)?
561 .collect::<Result<Vec<_>, _>>()
562 .map_err(super::sqlite_error)?;
563 drop(statement);
564 for ceremony_id in ids {
565 let stored = load_ceremony_connection(connection, &ceremony_id)?
566 .ok_or_else(|| invalid("ceremony disappeared during invariant verification"))?;
567 verify_stored_row(&stored)?;
568 let latest = connection
569 .query_row(
570 r#"
571 SELECT projection_sequence, record_digest
572 FROM frost_projection_commits
573 WHERE projection_key = ?1
574 ORDER BY projection_sequence DESC LIMIT 1
575 "#,
576 [projection_key(&ceremony_id)],
577 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
578 )
579 .optional()
580 .map_err(super::sqlite_error)?
581 .ok_or_else(|| invalid("ceremony has no projection commit"))?;
582 if read_u64(latest.0, "projection sequence")? != stored.state_version
583 || latest.1 != stored.record_digest
584 {
585 return Err(invalid(
586 "ceremony does not match its latest projection commit",
587 ));
588 }
589 }
590 verify_projection_commit_chains(connection)
591}
592
593fn insert_ceremony(
594 transaction: &Transaction<'_>,
595 write: &CeremonyWrite<'_>,
596 digest: &str,
597) -> Result<(), FrostStoreError> {
598 let changed = transaction
599 .execute(
600 r#"
601 INSERT INTO frost_ceremonies (
602 ceremony_id, config_json, config_digest, participant_set_digest,
603 scope_id, key_epoch, local_participant_id, state, state_version,
604 custody_generation, secret_kind, secret_nonce, secret_ciphertext,
605 output_nonce, output_ciphertext, input_transcript_digest,
606 public_key_package, group_public_key, verification_shares_json,
607 source_store_uuid, source_lease_id, source_owner_epoch,
608 updated_at_unix_ms, completed_at_unix_ms, record_digest
609 ) VALUES (
610 ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10,
611 ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19,
612 ?20, ?21, ?22, ?23, ?24, ?25
613 )
614 "#,
615 rusqlite::params_from_iter(ceremony_param_values(write, digest)?),
616 )
617 .map_err(super::sqlite_error)?;
618 if changed != 1 {
619 return Err(FrostStoreError::Conflict("ceremony insert lost its CAS"));
620 }
621 Ok(())
622}
623
624fn update_ceremony(
625 transaction: &Transaction<'_>,
626 write: &CeremonyWrite<'_>,
627 digest: &str,
628 expected_version: u64,
629) -> Result<(), FrostStoreError> {
630 let changed = transaction
631 .execute(
632 r#"
633 UPDATE frost_ceremonies SET
634 config_json = ?2, config_digest = ?3, participant_set_digest = ?4,
635 scope_id = ?5, key_epoch = ?6, local_participant_id = ?7,
636 state = ?8, state_version = ?9, custody_generation = ?10,
637 secret_kind = ?11, secret_nonce = ?12, secret_ciphertext = ?13,
638 output_nonce = ?14, output_ciphertext = ?15,
639 input_transcript_digest = ?16, public_key_package = ?17,
640 group_public_key = ?18, verification_shares_json = ?19,
641 source_store_uuid = ?20, source_lease_id = ?21,
642 source_owner_epoch = ?22, updated_at_unix_ms = ?23,
643 completed_at_unix_ms = ?24, record_digest = ?25
644 WHERE ceremony_id = ?1 AND state_version = ?26
645 "#,
646 rusqlite::params_from_iter(ceremony_param_values(write, digest)?.into_iter().chain([
647 rusqlite::types::Value::Integer(sqlite_u64(
648 expected_version,
649 "expected ceremony version",
650 )?),
651 ])),
652 )
653 .map_err(super::sqlite_error)?;
654 if changed != 1 {
655 return Err(FrostStoreError::Fenced);
656 }
657 Ok(())
658}
659
660fn ceremony_param_values(
661 write: &CeremonyWrite<'_>,
662 digest: &str,
663) -> Result<Vec<rusqlite::types::Value>, FrostStoreError> {
664 use rusqlite::types::Value;
665 let completed_at = match write.completed_at_unix_ms {
666 Some(value) => Value::Integer(sqlite_u64(value, "ceremony completion time")?),
667 None => Value::Null,
668 };
669 Ok(vec![
670 Value::Text(write.ceremony_id.to_string()),
671 Value::Blob(write.config_json.to_vec()),
672 Value::Text(write.config_digest.to_string()),
673 Value::Text(write.participant_set_digest.to_string()),
674 Value::Text(write.scope_id.to_string()),
675 Value::Integer(sqlite_u64(write.key_epoch, "ceremony key epoch")?),
676 Value::Text(write.local_participant_id.to_string()),
677 Value::Text(write.state.as_str().to_string()),
678 Value::Integer(sqlite_u64(write.state_version, "ceremony state version")?),
679 Value::Text(write.custody_generation.to_string()),
680 Value::Text(write.secret_kind.to_string()),
681 Value::Blob(write.secret.nonce.to_vec()),
682 Value::Blob(write.secret.ciphertext.clone()),
683 write
684 .output
685 .map_or(Value::Null, |output| Value::Blob(output.nonce.to_vec())),
686 write
687 .output
688 .map_or(Value::Null, |output| Value::Blob(output.ciphertext.clone())),
689 write
690 .input_transcript_digest
691 .map_or(Value::Null, |value| Value::Text(value.to_string())),
692 write
693 .public_key_package
694 .map_or(Value::Null, |value| Value::Blob(value.to_vec())),
695 write
696 .group_public_key
697 .map_or(Value::Null, |value| Value::Text(value.to_string())),
698 write
699 .verification_shares_json
700 .map_or(Value::Null, |value| Value::Blob(value.to_vec())),
701 Value::Text(write.source_fence.store_uuid.clone()),
702 Value::Text(write.source_fence.lease_id.clone()),
703 Value::Integer(sqlite_u64(
704 write.source_fence.owner_epoch,
705 "ceremony source owner epoch",
706 )?),
707 Value::Integer(sqlite_u64(
708 write.updated_at_unix_ms,
709 "ceremony updated time",
710 )?),
711 completed_at,
712 Value::Text(digest.to_string()),
713 ])
714}
715
716fn load_ceremony_tx(
717 transaction: &Transaction<'_>,
718 ceremony_id: &str,
719) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
720 load_ceremony_query(transaction, ceremony_id)
721}
722
723fn load_ceremony_connection(
724 connection: &Connection,
725 ceremony_id: &str,
726) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
727 load_ceremony_query(connection, ceremony_id)
728}
729
730fn load_ceremony_query(
731 connection: &Connection,
732 ceremony_id: &str,
733) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
734 let row = connection
735 .query_row(
736 r#"
737 SELECT ceremony_id, config_json, config_digest, participant_set_digest,
738 scope_id, key_epoch, local_participant_id, state, state_version,
739 custody_generation, secret_kind, secret_nonce, secret_ciphertext,
740 output_nonce, output_ciphertext, input_transcript_digest,
741 public_key_package, group_public_key, verification_shares_json,
742 source_store_uuid, source_lease_id, source_owner_epoch,
743 updated_at_unix_ms, completed_at_unix_ms, record_digest
744 FROM frost_ceremonies WHERE ceremony_id = ?1
745 "#,
746 [ceremony_id],
747 decode_row,
748 )
749 .optional()
750 .map_err(super::sqlite_error)?;
751 if let Some(stored) = row.as_ref() {
752 verify_stored_row(stored)?;
753 }
754 Ok(row)
755}
756
757fn decode_row(row: &Row<'_>) -> Result<StoredCeremonyRow, rusqlite::Error> {
758 let secret_nonce = nonce_from_row(row, 11)?;
759 let output_nonce = row
760 .get::<_, Option<Vec<u8>>>(13)?
761 .map(|bytes| nonce_from_bytes(bytes, 13))
762 .transpose()?;
763 let output_ciphertext = row.get::<_, Option<Vec<u8>>>(14)?;
764 let output = match (output_nonce, output_ciphertext) {
765 (Some(nonce), Some(ciphertext)) => Some(EncryptedBlob { nonce, ciphertext }),
766 (None, None) => None,
767 _ => {
768 return Err(rusqlite::Error::InvalidColumnType(
769 13,
770 "output_nonce".to_string(),
771 rusqlite::types::Type::Null,
772 ));
773 }
774 };
775 Ok(StoredCeremonyRow {
776 ceremony_id: row.get(0)?,
777 config_json: row.get(1)?,
778 config_digest: row.get(2)?,
779 participant_set_digest: row.get(3)?,
780 scope_id: row.get(4)?,
781 key_epoch: sqlite_read_u64(row, 5)?,
782 local_participant_id: row.get(6)?,
783 state: FrostCeremonyState::parse(&row.get::<_, String>(7)?)
784 .map_err(|error| rusqlite::Error::InvalidParameterName(error.to_string()))?,
785 state_version: sqlite_read_u64(row, 8)?,
786 custody_generation: row.get(9)?,
787 secret_kind: row.get(10)?,
788 secret: EncryptedBlob {
789 nonce: secret_nonce,
790 ciphertext: row.get(12)?,
791 },
792 output,
793 input_transcript_digest: row.get(15)?,
794 public_key_package: row.get(16)?,
795 group_public_key: row.get(17)?,
796 verification_shares_json: row.get(18)?,
797 source_fence: StoreMutationFence {
798 store_uuid: row.get(19)?,
799 lease_id: row.get(20)?,
800 owner_epoch: sqlite_read_u64(row, 21)?,
801 },
802 updated_at_unix_ms: sqlite_read_u64(row, 22)?,
803 completed_at_unix_ms: row
804 .get::<_, Option<i64>>(23)?
805 .map(|value| {
806 u64::try_from(value)
807 .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(23, value))
808 })
809 .transpose()?,
810 record_digest: row.get(24)?,
811 })
812}
813
814fn verify_stored_row(stored: &StoredCeremonyRow) -> Result<(), FrostStoreError> {
815 let config: FrostCeremonyConfig = serde_json::from_slice(&stored.config_json)
816 .map_err(|error| invalid(format!("ceremony config does not decode: {error}")))?;
817 config.validate()?;
818 if config.ceremony_id()? != stored.ceremony_id
819 || sha256_hex(&stored.config_json) != stored.config_digest
820 || config.participant_set_digest()? != stored.participant_set_digest
821 || config.scope_id != stored.scope_id
822 || config.key_epoch != stored.key_epoch
823 || config.local_participant_id != stored.local_participant_id
824 {
825 return Err(invalid("ceremony config binding is invalid"));
826 }
827 let expected_version = match stored.state {
828 FrostCeremonyState::Round1Ready => 1,
829 FrostCeremonyState::Round2Ready => 2,
830 FrostCeremonyState::Completed => 3,
831 };
832 if stored.state_version != expected_version {
833 return Err(invalid("ceremony state version is invalid"));
834 }
835 let write = row_as_write(stored);
836 if record_digest(&write)? != stored.record_digest {
837 return Err(invalid("ceremony record digest does not match"));
838 }
839 Ok(())
840}
841
842fn verify_exact_config(
843 stored: &StoredCeremonyRow,
844 config_digest: &str,
845 custody: &FrostCustodyKey,
846 trusted_now_unix_ms: u64,
847) -> Result<(), FrostStoreError> {
848 if stored.config_digest != config_digest {
849 return Err(FrostStoreError::Conflict("ceremony configuration changed"));
850 }
851 if stored.custody_generation != custody.generation() {
852 return Err(FrostStoreError::Custody(
853 "custody generation does not match the ceremony",
854 ));
855 }
856 if trusted_now_unix_ms < stored.updated_at_unix_ms {
857 return Err(FrostStoreError::Conflict(
858 "trusted time regressed behind the ceremony",
859 ));
860 }
861 Ok(())
862}
863
864fn decrypt_secret(
865 stored: &StoredCeremonyRow,
866 custody: &FrostCustodyKey,
867) -> Result<FrostCeremonySecret, FrostStoreError> {
868 verify_custody_generation(stored, custody)?;
869 let plaintext = decrypt_blob_with_aad(
870 custody.key(),
871 &stored.secret,
872 &custody_aad("secret", &stored.custody_binding())?,
873 )
874 .map_err(|_| FrostStoreError::Custody("ceremony secret authentication failed"))?;
875 FrostCeremonySecret::from_custody_bytes(
876 parse_secret_kind(&stored.secret_kind)?,
877 Zeroizing::new(plaintext),
878 )
879 .map_err(Into::into)
880}
881
882fn decrypt_output(
883 stored: &StoredCeremonyRow,
884 custody: &FrostCustodyKey,
885) -> Result<StoredCeremonyOutput, FrostStoreError> {
886 verify_custody_generation(stored, custody)?;
887 let output = stored
888 .output
889 .as_ref()
890 .ok_or_else(|| invalid("ceremony output is absent"))?;
891 let plaintext = Zeroizing::new(
892 decrypt_blob_with_aad(
893 custody.key(),
894 output,
895 &custody_aad("output", &stored.custody_binding())?,
896 )
897 .map_err(|_| FrostStoreError::Custody("ceremony output authentication failed"))?,
898 );
899 serde_json::from_slice(&plaintext)
900 .map_err(|error| invalid(format!("ceremony output does not decode: {error}")))
901}
902
903fn completion_from_row(
904 stored: &StoredCeremonyRow,
905) -> Result<StoredFrostCeremonyCompletion, FrostStoreError> {
906 let verification_shares = serde_json::from_slice::<BTreeMap<String, String>>(
907 stored
908 .verification_shares_json
909 .as_deref()
910 .ok_or_else(|| invalid("completed ceremony lacks verification shares"))?,
911 )
912 .map_err(|error| invalid(format!("verification shares do not decode: {error}")))?;
913 Ok(StoredFrostCeremonyCompletion {
914 ceremony_id: stored.ceremony_id.clone(),
915 state: stored.state,
916 state_version: stored.state_version,
917 public_key_package: stored
918 .public_key_package
919 .clone()
920 .ok_or_else(|| invalid("completed ceremony lacks a public key package"))?,
921 group_public_key: stored
922 .group_public_key
923 .clone()
924 .ok_or_else(|| invalid("completed ceremony lacks a group public key"))?,
925 verification_shares,
926 transcript_digest: stored
927 .input_transcript_digest
928 .clone()
929 .ok_or_else(|| invalid("completed ceremony lacks a transcript digest"))?,
930 })
931}
932
933fn public_record(stored: &StoredCeremonyRow) -> FrostCeremonyRecord {
934 FrostCeremonyRecord {
935 ceremony_id: stored.ceremony_id.clone(),
936 state: stored.state,
937 state_version: stored.state_version,
938 participant_set_digest: stored.participant_set_digest.clone(),
939 scope_id: stored.scope_id.clone(),
940 key_epoch: stored.key_epoch,
941 local_participant_id: stored.local_participant_id.clone(),
942 input_transcript_digest: stored.input_transcript_digest.clone(),
943 }
944}
945
946fn row_as_write(stored: &StoredCeremonyRow) -> CeremonyWrite<'_> {
947 CeremonyWrite {
948 ceremony_id: &stored.ceremony_id,
949 config_json: &stored.config_json,
950 config_digest: &stored.config_digest,
951 participant_set_digest: &stored.participant_set_digest,
952 scope_id: &stored.scope_id,
953 key_epoch: stored.key_epoch,
954 local_participant_id: &stored.local_participant_id,
955 state: stored.state,
956 state_version: stored.state_version,
957 custody_generation: &stored.custody_generation,
958 secret_kind: &stored.secret_kind,
959 secret: &stored.secret,
960 output: stored.output.as_ref(),
961 input_transcript_digest: stored.input_transcript_digest.as_deref(),
962 public_key_package: stored.public_key_package.as_deref(),
963 group_public_key: stored.group_public_key.as_deref(),
964 verification_shares_json: stored.verification_shares_json.as_deref(),
965 source_fence: &stored.source_fence,
966 updated_at_unix_ms: stored.updated_at_unix_ms,
967 completed_at_unix_ms: stored.completed_at_unix_ms,
968 }
969}
970
971fn record_digest(write: &CeremonyWrite<'_>) -> Result<String, FrostStoreError> {
972 let preimage = RecordDigestPreimage {
973 format: "chio.frost.ceremony-record.v1",
974 ceremony_id: write.ceremony_id,
975 config_digest: write.config_digest,
976 participant_set_digest: write.participant_set_digest,
977 scope_id: write.scope_id,
978 key_epoch: write.key_epoch,
979 local_participant_id: write.local_participant_id,
980 state: write.state,
981 state_version: write.state_version,
982 custody_generation: write.custody_generation,
983 secret_kind: write.secret_kind,
984 secret_nonce: hex::encode(write.secret.nonce),
985 secret_ciphertext_digest: sha256_hex(&write.secret.ciphertext),
986 output_nonce: write.output.map(|output| hex::encode(output.nonce)),
987 output_ciphertext_digest: write.output.map(|output| sha256_hex(&output.ciphertext)),
988 input_transcript_digest: write.input_transcript_digest,
989 public_key_package_digest: write.public_key_package.map(sha256_hex),
990 group_public_key: write.group_public_key,
991 verification_shares_digest: write.verification_shares_json.map(sha256_hex),
992 store_uuid: &write.source_fence.store_uuid,
993 store_lease_id: &write.source_fence.lease_id,
994 store_owner_epoch: write.source_fence.owner_epoch,
995 updated_at_unix_ms: write.updated_at_unix_ms,
996 completed_at_unix_ms: write.completed_at_unix_ms,
997 };
998 prefixed_digest(RECORD_DIGEST_PREFIX, &preimage)
999}
1000
1001fn custody_aad(purpose: &str, binding: &CustodyBinding<'_>) -> Result<Vec<u8>, FrostStoreError> {
1002 canonical_json_bytes(&CustodyAad {
1003 format: CUSTODY_AAD_FORMAT,
1004 purpose,
1005 ceremony_id: binding.ceremony_id,
1006 config_digest: binding.config_digest,
1007 state: binding.state,
1008 state_version: binding.state_version,
1009 custody_generation: binding.custody_generation,
1010 store_uuid: &binding.fence.store_uuid,
1011 store_lease_id: &binding.fence.lease_id,
1012 store_owner_epoch: binding.fence.owner_epoch,
1013 updated_at_unix_ms: binding.updated_at_unix_ms,
1014 })
1015 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))
1016}
1017
1018fn encrypt_material(
1019 plaintext: &[u8],
1020 custody: &FrostCustodyKey,
1021 aad: &[u8],
1022) -> Result<EncryptedBlob, FrostStoreError> {
1023 try_encrypt_blob_with_aad(custody.key(), plaintext, aad)
1024 .map_err(|_| FrostStoreError::Custody("ceremony material encryption failed"))
1025}
1026
1027fn verify_custody_generation(
1028 stored: &StoredCeremonyRow,
1029 custody: &FrostCustodyKey,
1030) -> Result<(), FrostStoreError> {
1031 if stored.custody_generation != custody.generation() {
1032 return Err(FrostStoreError::Custody(
1033 "custody generation does not match the ceremony",
1034 ));
1035 }
1036 Ok(())
1037}
1038
1039fn parse_secret_kind(value: &str) -> Result<FrostCeremonySecretKind, FrostStoreError> {
1040 match value {
1041 "round1" => Ok(FrostCeremonySecretKind::Round1),
1042 "round2" => Ok(FrostCeremonySecretKind::Round2),
1043 "key_package" => Ok(FrostCeremonySecretKind::KeyPackage),
1044 _ => Err(invalid("ceremony secret kind is unknown")),
1045 }
1046}
1047
1048fn projection_key(ceremony_id: &str) -> String {
1049 format!("ceremony/{ceremony_id}")
1050}
1051
1052fn validate_trusted_time(value: u64) -> Result<(), FrostStoreError> {
1053 if value == 0 || i64::try_from(value).is_err() {
1054 return Err(FrostStoreError::Conflict(
1055 "trusted time is outside the SQLite range",
1056 ));
1057 }
1058 Ok(())
1059}
1060
1061fn nonce_from_row(row: &Row<'_>, index: usize) -> Result<[u8; 12], rusqlite::Error> {
1062 nonce_from_bytes(row.get(index)?, index)
1063}
1064
1065fn nonce_from_bytes(bytes: Vec<u8>, index: usize) -> Result<[u8; 12], rusqlite::Error> {
1066 let length = bytes.len();
1067 bytes.try_into().map_err(|_| {
1068 rusqlite::Error::FromSqlConversionFailure(
1069 index,
1070 rusqlite::types::Type::Blob,
1071 format!("FROST nonce must be 12 bytes, got {length}").into(),
1072 )
1073 })
1074}
1075
1076fn sqlite_read_u64(row: &Row<'_>, index: usize) -> Result<u64, rusqlite::Error> {
1077 let value = row.get::<_, i64>(index)?;
1078 u64::try_from(value).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(index, value))
1079}
1080
1081fn sqlite_u64(value: u64, field: &'static str) -> Result<i64, FrostStoreError> {
1082 i64::try_from(value)
1083 .map_err(|_| FrostStoreError::InvalidState(format!("{field} exceeds SQLite range")))
1084}
1085
1086fn read_u64(value: i64, field: &'static str) -> Result<u64, FrostStoreError> {
1087 u64::try_from(value).map_err(|_| FrostStoreError::InvalidState(format!("{field} is negative")))
1088}
1089
1090fn invalid(detail: impl Into<String>) -> FrostStoreError {
1091 FrostStoreError::InvalidState(detail.into())
1092}