1use chio_core::canonical::canonical_json_bytes;
2use chio_core::{sha256_hex, StoreMutationFence};
3use chio_federation::frost::{
4 frost_authorization_session_id, frost_authorization_slot_id,
5 verify_bound_frost_authorization_slot, verify_burned_frost_authorization_slot,
6 verify_completed_frost_authorization_slot, verify_frost_authorization_slot_bind,
7 verify_frost_authorization_slot_burn, FrostAuthorizationSlotCheckpointV1,
8 FrostAuthorizationSlotState,
9};
10use chio_federation_authority::{
11 create_frost_signature_share, inspect_frost_signer_key, prepare_frost_signer,
12 FrostCeremonySecret, FrostSignerNonceSecret,
13};
14use rand_core::{CryptoRng, RngCore};
15use rusqlite::{Connection, OptionalExtension, Row};
16use serde::Serialize;
17use zeroize::Zeroizing;
18
19use super::{
20 FrostCeremonyState, FrostCustodyKey, FrostSignerCommitment, FrostSignerSessionRecord,
21 FrostSignerSessionRequest, FrostSignerSessionState, FrostSignerShare, FrostStoreError,
22 SqliteFrostStore,
23};
24use crate::encrypted_blob::{decrypt_blob_with_aad, try_encrypt_blob_with_aad, EncryptedBlob};
25
26mod persistence;
27
28use persistence::{
29 append_signer_commit, insert_signer, load_signer_by_slot_query, load_signer_query,
30 signer_record_digest, update_signer,
31};
32
33const SIGNER_RECORD_PREFIX: &[u8] = b"chio.frost.signer-record.digest.v1\0";
34const SIGNER_AAD_FORMAT: &str = "chio.frost.signer-nonce-aad.v1";
35const MAX_SIGNING_PACKAGE_BYTES: usize = 1024 * 1024;
36const MAX_BURN_REASON_BYTES: usize = 256;
37
38#[derive(Debug)]
39struct ValidatedRequest {
40 session_id: String,
41 authorization_slot_id: String,
42 authorization_body_json: Vec<u8>,
43 signing_message_digest: String,
44}
45
46#[derive(Debug, Clone)]
47struct StoredSigner {
48 session_id: String,
49 participant_id: String,
50 authorization_slot_id: String,
51 scope_id: String,
52 key_epoch: u64,
53 authorization_id: String,
54 signing_message_digest: String,
55 roster_digest: String,
56 coordinator_id: String,
57 resource_fence: u64,
58 ceremony_id: String,
59 authorization_body_json: Vec<u8>,
60 bound_checkpoint_digest: String,
61 bound_checkpoint_json: Vec<u8>,
62 signer_identifier: Vec<u8>,
63 verification_share: String,
64 state: FrostSignerSessionState,
65 state_version: u64,
66 custody_generation: String,
67 nonce_aad: Vec<u8>,
68 nonce: Option<EncryptedBlob>,
69 commitment_bytes: Vec<u8>,
70 commitment_digest: String,
71 signing_package_digest: Option<String>,
72 signature_share: Option<Vec<u8>>,
73 burn_reason: Option<String>,
74 source_fence: StoreMutationFence,
75 created_at_unix_ms: u64,
76 updated_at_unix_ms: u64,
77 record_digest: String,
78}
79
80#[derive(Serialize)]
81#[serde(rename_all = "camelCase")]
82struct SignerNonceAad<'a> {
83 format: &'static str,
84 participant_id: &'a str,
85 scope_id: &'a str,
86 key_epoch: u64,
87 session_id: &'a str,
88 authorization_id: &'a str,
89 signing_message_digest: &'a str,
90 coordinator_id: &'a str,
91 resource_fence: u64,
92 store_uuid: &'a str,
93 store_lease_id: &'a str,
94 store_owner_epoch: u64,
95}
96
97#[derive(Serialize)]
98#[serde(rename_all = "camelCase")]
99struct SignerRecordPreimage<'a> {
100 format: &'static str,
101 session_id: &'a str,
102 participant_id: &'a str,
103 authorization_slot_id: &'a str,
104 scope_id: &'a str,
105 key_epoch: u64,
106 authorization_id: &'a str,
107 signing_message_digest: &'a str,
108 roster_digest: &'a str,
109 coordinator_id: &'a str,
110 resource_fence: u64,
111 ceremony_id: &'a str,
112 authorization_body_json_digest: String,
113 bound_checkpoint_digest: &'a str,
114 bound_checkpoint_json_digest: String,
115 signer_identifier: String,
116 verification_share: &'a str,
117 state: FrostSignerSessionState,
118 state_version: u64,
119 custody_generation: &'a str,
120 nonce_aad_digest: String,
121 nonce_ciphertext_digest: Option<String>,
122 nonce_nonce: Option<String>,
123 commitment_digest: String,
124 signing_package_digest: Option<&'a str>,
125 signature_share_digest: Option<String>,
126 burn_reason: Option<&'a str>,
127 source_store_uuid: &'a str,
128 source_lease_id: &'a str,
129 source_owner_epoch: u64,
130 created_at_unix_ms: u64,
131 updated_at_unix_ms: u64,
132}
133
134impl SqliteFrostStore {
135 pub fn prepare_signer_session<R: CryptoRng + RngCore>(
136 &self,
137 request: &FrostSignerSessionRequest<'_>,
138 custody: &FrostCustodyKey,
139 rng: &mut R,
140 fence: &StoreMutationFence,
141 trusted_now_unix_ms: u64,
142 ) -> Result<FrostSignerSessionRecord, FrostStoreError> {
143 validate_time(trusted_now_unix_ms)?;
144 let validated = validate_request(request)?;
145 if let Some(mut stored) = self.load_signer_by_slot(
146 &validated.authorization_slot_id,
147 request.participant_id,
148 Some(fence),
149 )? {
150 if !matches_request(&stored, request, &validated) {
151 self.burn_stored(
152 stored,
153 request,
154 fence,
155 trusted_now_unix_ms,
156 "signer input changed after slot binding",
157 )?;
158 return Err(FrostStoreError::Conflict(
159 "signer input changed after slot binding",
160 ));
161 }
162 verify_custody(&stored, custody)?;
163 verify_active_request(request)?;
164 self.ensure_stored_slot_bound(request, &mut stored, fence, trusted_now_unix_ms)?;
165 return Ok(public_record(&stored));
166 }
167
168 verify_active_request(request)?;
169 let key_package = self.load_signer_key_package(request, custody, fence)?;
170 let expected_share = request
171 .active_roster
172 .participant_verification_share(request.participant_id)
173 .ok_or(FrostStoreError::Conflict(
174 "signer participant is absent from the active roster",
175 ))?;
176 let key_identity =
177 inspect_frost_signer_key(&key_package).map_err(|error| invalid(error.to_string()))?;
178 if key_identity.verification_share() != expected_share {
179 return Err(FrostStoreError::Conflict(
180 "local key package does not match the active roster",
181 ));
182 }
183 let bind = verify_frost_authorization_slot_bind(
184 request.body,
185 request.active_roster,
186 request.epoch_anchor,
187 request.artifact_trust,
188 trusted_now_unix_ms,
189 )
190 .map_err(transition_error)?;
191 let anchored = request
192 .slot_anchor
193 .compare_and_swap_bind(&bind)
194 .map_err(anchor_error)?;
195 let bound = verify_bound_frost_authorization_slot(
196 request.body,
197 request.active_roster,
198 request.epoch_anchor,
199 &anchored,
200 request.artifact_trust,
201 trusted_now_unix_ms,
202 )
203 .map_err(transition_error)?;
204 let preparation = prepare_frost_signer(&key_package, rng)
205 .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
206 if preparation.verification_share() != expected_share {
207 return Err(FrostStoreError::Conflict(
208 "local key package does not match the active roster",
209 ));
210 }
211 let bound_checkpoint_json =
212 canonical_json_bytes(bound.checkpoint()).map_err(|error| invalid(error.to_string()))?;
213 let nonce_aad = signer_nonce_aad(request, &validated, fence)?;
214 let nonce = try_encrypt_blob_with_aad(
215 custody.key(),
216 preparation.nonce_secret().custody_bytes(),
217 &nonce_aad,
218 )
219 .map_err(|()| FrostStoreError::Custody("signer nonce encryption failed"))?;
220 let mut stored = StoredSigner {
221 session_id: validated.session_id,
222 participant_id: request.participant_id.to_string(),
223 authorization_slot_id: validated.authorization_slot_id,
224 scope_id: request.body.scope_id.clone(),
225 key_epoch: request.body.key_epoch,
226 authorization_id: request.body.authorization_id.clone(),
227 signing_message_digest: validated.signing_message_digest,
228 roster_digest: request.body.roster_digest.clone(),
229 coordinator_id: request.coordinator_id.to_string(),
230 resource_fence: request.body.resource_fence,
231 ceremony_id: request.ceremony_id.to_string(),
232 authorization_body_json: validated.authorization_body_json,
233 bound_checkpoint_digest: bound.checkpoint_digest().to_string(),
234 bound_checkpoint_json,
235 signer_identifier: preparation.signer_identifier_bytes().to_vec(),
236 verification_share: preparation.verification_share().to_string(),
237 state: FrostSignerSessionState::Prepared,
238 state_version: 1,
239 custody_generation: custody.generation().to_string(),
240 nonce_aad,
241 nonce: Some(nonce),
242 commitment_bytes: preparation.commitment_bytes().to_vec(),
243 commitment_digest: sha256_hex(preparation.commitment_bytes()),
244 signing_package_digest: None,
245 signature_share: None,
246 burn_reason: None,
247 source_fence: fence.clone(),
248 created_at_unix_ms: trusted_now_unix_ms,
249 updated_at_unix_ms: trusted_now_unix_ms,
250 record_digest: String::new(),
251 };
252 stored.record_digest = signer_record_digest(&stored)?;
253 let mut connection = self.connection()?;
254 let transaction = self.begin_write(&mut connection, fence)?;
255 if let Some(mut existing) = load_signer_by_slot_query(
256 &transaction,
257 &stored.authorization_slot_id,
258 &stored.participant_id,
259 )? {
260 if matches_request(&existing, request, &validate_request(request)?) {
261 transaction.commit().map_err(super::sqlite_error)?;
262 drop(connection);
263 verify_custody(&existing, custody)?;
264 verify_active_request(request)?;
265 self.ensure_stored_slot_bound(request, &mut existing, fence, trusted_now_unix_ms)?;
266 return Ok(public_record(&existing));
267 }
268 return Err(FrostStoreError::Conflict(
269 "another signer session claimed the authorization slot",
270 ));
271 }
272 insert_signer(&transaction, &stored)?;
273 append_signer_commit(&transaction, self, &stored, "frost.signer.prepare", fence)?;
274 self.commit_write(transaction)?;
275 self.sync_after_write(&connection)?;
276 Ok(public_record(&stored))
277 }
278
279 pub fn publish_signer_commitment(
280 &self,
281 request: &FrostSignerSessionRequest<'_>,
282 custody: &FrostCustodyKey,
283 fence: &StoreMutationFence,
284 trusted_now_unix_ms: u64,
285 ) -> Result<FrostSignerCommitment, FrostStoreError> {
286 let mut stored = self.load_exact_live(request, fence, trusted_now_unix_ms)?;
287 verify_custody(&stored, custody)?;
288 if stored.state == FrostSignerSessionState::Prepared {
289 let previous_state = stored.state;
290 let previous_version = stored.state_version;
291 stored.state = FrostSignerSessionState::CommitmentPublished;
292 stored.state_version = 2;
293 advance_record(&mut stored, fence, trusted_now_unix_ms)?;
294 self.persist_transition(
295 &stored,
296 previous_state,
297 previous_version,
298 "frost.signer.publish_commitment",
299 fence,
300 )?;
301 } else if !matches!(
302 stored.state,
303 FrostSignerSessionState::CommitmentPublished
304 | FrostSignerSessionState::ShareReady
305 | FrostSignerSessionState::Completed
306 ) {
307 return Err(FrostStoreError::Conflict(
308 "signer commitment is unavailable in the current state",
309 ));
310 }
311 Ok(public_commitment(&stored))
312 }
313
314 pub fn prepare_signer_share(
315 &self,
316 request: &FrostSignerSessionRequest<'_>,
317 signing_package_bytes: &[u8],
318 custody: &FrostCustodyKey,
319 fence: &StoreMutationFence,
320 trusted_now_unix_ms: u64,
321 ) -> Result<FrostSignerSessionRecord, FrostStoreError> {
322 if signing_package_bytes.is_empty()
323 || signing_package_bytes.len() > MAX_SIGNING_PACKAGE_BYTES
324 {
325 return Err(FrostStoreError::Conflict(
326 "signing package length is outside the supported range",
327 ));
328 }
329 let mut stored = self.load_exact_live(request, fence, trusted_now_unix_ms)?;
330 verify_custody(&stored, custody)?;
331 let package_digest = sha256_hex(signing_package_bytes);
332 if matches!(
333 stored.state,
334 FrostSignerSessionState::ShareReady | FrostSignerSessionState::Completed
335 ) {
336 if stored.signing_package_digest.as_deref() == Some(package_digest.as_str()) {
337 return Ok(public_record(&stored));
338 }
339 self.burn_stored(
340 stored,
341 request,
342 fence,
343 trusted_now_unix_ms,
344 "signing package changed after share creation",
345 )?;
346 return Err(FrostStoreError::Conflict(
347 "signing package changed after share creation",
348 ));
349 }
350 if stored.state != FrostSignerSessionState::CommitmentPublished {
351 return Err(FrostStoreError::Conflict(
352 "signer commitment has not been published",
353 ));
354 }
355 let nonce = stored
356 .nonce
357 .as_ref()
358 .ok_or_else(|| invalid("live signer lacks encrypted nonce"))?;
359 let nonce_plaintext = decrypt_blob_with_aad(custody.key(), nonce, &stored.nonce_aad)
360 .map_err(|_| FrostStoreError::Custody("signer nonce authentication failed"))?;
361 let nonce_secret =
362 FrostSignerNonceSecret::from_custody_bytes(Zeroizing::new(nonce_plaintext))
363 .map_err(|error| invalid(error.to_string()))?;
364 let key_package = self.load_signer_key_package(request, custody, fence)?;
365 let share = match create_frost_signature_share(
366 &key_package,
367 nonce_secret,
368 signing_package_bytes,
369 &stored.signing_message_digest,
370 &stored.commitment_bytes,
371 ) {
372 Ok(share) => share,
373 Err(error) => {
374 let detail = error.to_string();
375 self.burn_stored(
376 stored,
377 request,
378 fence,
379 trusted_now_unix_ms,
380 "signing package failed exact-message verification",
381 )?;
382 return Err(invalid(detail));
383 }
384 };
385 let previous_state = stored.state;
386 let previous_version = stored.state_version;
387 stored.state = FrostSignerSessionState::ShareReady;
388 stored.state_version = 3;
389 stored.signing_package_digest = Some(package_digest);
390 stored.signature_share = Some(share.share_bytes().to_vec());
391 advance_record(&mut stored, fence, trusted_now_unix_ms)?;
392 self.persist_transition(
393 &stored,
394 previous_state,
395 previous_version,
396 "frost.signer.prepare_share",
397 fence,
398 )?;
399 Ok(public_record(&stored))
400 }
401
402 pub fn publish_signer_share(
403 &self,
404 request: &FrostSignerSessionRequest<'_>,
405 custody: &FrostCustodyKey,
406 fence: &StoreMutationFence,
407 trusted_now_unix_ms: u64,
408 ) -> Result<FrostSignerShare, FrostStoreError> {
409 let stored = self.load_exact_live(request, fence, trusted_now_unix_ms)?;
410 verify_custody(&stored, custody)?;
411 if !matches!(
412 stored.state,
413 FrostSignerSessionState::ShareReady | FrostSignerSessionState::Completed
414 ) {
415 return Err(FrostStoreError::Conflict(
416 "signature share is unavailable in the current state",
417 ));
418 }
419 let share_bytes = stored
420 .signature_share
421 .clone()
422 .ok_or_else(|| invalid("share-ready signer lacks signature share"))?;
423 Ok(FrostSignerShare {
424 session_id: stored.session_id,
425 participant_id: stored.participant_id,
426 share_bytes,
427 })
428 }
429
430 pub fn complete_signer_session(
431 &self,
432 request: &FrostSignerSessionRequest<'_>,
433 custody: &FrostCustodyKey,
434 fence: &StoreMutationFence,
435 trusted_now_unix_ms: u64,
436 ) -> Result<FrostSignerSessionRecord, FrostStoreError> {
437 let mut stored = self.load_exact_live(request, fence, trusted_now_unix_ms)?;
438 verify_custody(&stored, custody)?;
439 if stored.state == FrostSignerSessionState::Completed {
440 return Ok(public_record(&stored));
441 }
442 if stored.state != FrostSignerSessionState::ShareReady {
443 return Err(FrostStoreError::Conflict(
444 "signer share is not ready for completion",
445 ));
446 }
447 let previous_state = stored.state;
448 let previous_version = stored.state_version;
449 stored.state = FrostSignerSessionState::Completed;
450 stored.state_version = 4;
451 stored.nonce = None;
452 advance_record(&mut stored, fence, trusted_now_unix_ms)?;
453 self.persist_transition(
454 &stored,
455 previous_state,
456 previous_version,
457 "frost.signer.complete",
458 fence,
459 )?;
460 Ok(public_record(&stored))
461 }
462
463 pub fn burn_signer_session(
464 &self,
465 request: &FrostSignerSessionRequest<'_>,
466 reason: &str,
467 fence: &StoreMutationFence,
468 trusted_now_unix_ms: u64,
469 ) -> Result<FrostSignerSessionRecord, FrostStoreError> {
470 let validated = validate_request(request)?;
471 let stored = self
472 .load_signer_by_slot(
473 &validated.authorization_slot_id,
474 request.participant_id,
475 Some(fence),
476 )?
477 .ok_or(FrostStoreError::Conflict("signer session is absent"))?;
478 if !matches_request(&stored, request, &validated) {
479 return Err(FrostStoreError::Conflict(
480 "burn request differs from the signer session",
481 ));
482 }
483 self.burn_stored(stored, request, fence, trusted_now_unix_ms, reason)
484 }
485
486 pub fn load_signer_session(
487 &self,
488 session_id: &str,
489 participant_id: &str,
490 ) -> Result<Option<FrostSignerSessionRecord>, FrostStoreError> {
491 let mut connection = self.connection()?;
492 let transaction = self.begin_read(&mut connection, None)?;
493 let stored = load_signer_query(&transaction, session_id, participant_id)?;
494 transaction.commit().map_err(super::sqlite_error)?;
495 Ok(stored.as_ref().map(public_record))
496 }
497
498 fn load_exact_live(
499 &self,
500 request: &FrostSignerSessionRequest<'_>,
501 fence: &StoreMutationFence,
502 trusted_now_unix_ms: u64,
503 ) -> Result<StoredSigner, FrostStoreError> {
504 validate_time(trusted_now_unix_ms)?;
505 let validated = validate_request(request)?;
506 let mut stored = self
507 .load_signer_by_slot(
508 &validated.authorization_slot_id,
509 request.participant_id,
510 Some(fence),
511 )?
512 .ok_or(FrostStoreError::Conflict("signer session is absent"))?;
513 if !matches_request(&stored, request, &validated) {
514 self.burn_stored(
515 stored,
516 request,
517 fence,
518 trusted_now_unix_ms,
519 "signer input changed after slot binding",
520 )?;
521 return Err(FrostStoreError::Conflict(
522 "signer input changed after slot binding",
523 ));
524 }
525 if stored.state == FrostSignerSessionState::Burned {
526 return Err(FrostStoreError::Conflict("signer session is burned"));
527 }
528 verify_active_request(request)?;
529 self.ensure_stored_slot_bound(request, &mut stored, fence, trusted_now_unix_ms)?;
530 Ok(stored)
531 }
532
533 fn ensure_stored_slot_bound(
534 &self,
535 request: &FrostSignerSessionRequest<'_>,
536 stored: &mut StoredSigner,
537 fence: &StoreMutationFence,
538 trusted_now_unix_ms: u64,
539 ) -> Result<(), FrostStoreError> {
540 let slot_id = frost_authorization_slot_id(request.body)
541 .map_err(|error| invalid(error.to_string()))?;
542 let anchored = request
543 .slot_anchor
544 .resolve_authorization_slot(&request.body.scope_id, &slot_id)
545 .map_err(anchor_error)?;
546 match anchored.checkpoint.state {
547 FrostAuthorizationSlotState::Bound => {
548 let bound = verify_bound_frost_authorization_slot(
549 request.body,
550 request.active_roster,
551 request.epoch_anchor,
552 &anchored,
553 request.artifact_trust,
554 trusted_now_unix_ms,
555 )
556 .map_err(transition_error)?;
557 if bound.checkpoint_digest() != stored.bound_checkpoint_digest {
558 return Err(FrostStoreError::Conflict(
559 "external bound checkpoint changed after signer preparation",
560 ));
561 }
562 Ok(())
563 }
564 FrostAuthorizationSlotState::Burned => {
565 let bound: FrostAuthorizationSlotCheckpointV1 =
566 serde_json::from_slice(&stored.bound_checkpoint_json)
567 .map_err(|error| invalid(error.to_string()))?;
568 verify_burned_frost_authorization_slot(
569 &bound,
570 &anchored,
571 request.artifact_trust,
572 trusted_now_unix_ms,
573 )
574 .map_err(transition_error)?;
575 if !matches!(
576 stored.state,
577 FrostSignerSessionState::Completed | FrostSignerSessionState::Burned
578 ) {
579 self.reconcile_local_burn(
580 stored,
581 fence,
582 trusted_now_unix_ms,
583 "external authorization slot was already burned",
584 )?;
585 }
586 Err(FrostStoreError::Conflict(
587 "external authorization slot is burned",
588 ))
589 }
590 FrostAuthorizationSlotState::Completed => {
591 verify_completed_frost_authorization_slot(
592 request.body,
593 request.active_roster,
594 request.epoch_anchor,
595 &anchored,
596 request.artifact_trust,
597 &stored.bound_checkpoint_digest,
598 trusted_now_unix_ms,
599 )
600 .map_err(transition_error)?;
601 match stored.state {
602 FrostSignerSessionState::Completed => Ok(()),
603 FrostSignerSessionState::ShareReady => {
604 self.reconcile_local_completion(stored, fence, trusted_now_unix_ms)
605 }
606 FrostSignerSessionState::Prepared
607 | FrostSignerSessionState::CommitmentPublished => {
608 self.reconcile_local_burn(
609 stored,
610 fence,
611 trusted_now_unix_ms,
612 "external authorization completed without this signer share",
613 )?;
614 Err(FrostStoreError::Conflict(
615 "external authorization completed without this signer share",
616 ))
617 }
618 FrostSignerSessionState::Burned => {
619 Err(FrostStoreError::Conflict("signer session is burned"))
620 }
621 }
622 }
623 }
624 }
625
626 fn load_signer_key_package(
627 &self,
628 request: &FrostSignerSessionRequest<'_>,
629 custody: &FrostCustodyKey,
630 fence: &StoreMutationFence,
631 ) -> Result<FrostCeremonySecret, FrostStoreError> {
632 let ceremony = self
633 .load_ceremony(request.ceremony_id, custody)?
634 .ok_or(FrostStoreError::Conflict("signer ceremony is absent"))?;
635 if ceremony.state != FrostCeremonyState::Completed
636 || ceremony.scope_id != request.body.scope_id
637 || ceremony.key_epoch != request.body.key_epoch
638 || ceremony.local_participant_id != request.participant_id
639 {
640 return Err(FrostStoreError::Conflict(
641 "signer ceremony does not match the authorization",
642 ));
643 }
644 self.load_completed_key_package(request.ceremony_id, custody, fence)
645 }
646
647 fn load_signer_by_slot(
648 &self,
649 slot_id: &str,
650 participant_id: &str,
651 fence: Option<&StoreMutationFence>,
652 ) -> Result<Option<StoredSigner>, FrostStoreError> {
653 let mut connection = self.connection()?;
654 let transaction = self.begin_read(&mut connection, fence)?;
655 let stored = load_signer_by_slot_query(&transaction, slot_id, participant_id)?;
656 transaction.commit().map_err(super::sqlite_error)?;
657 Ok(stored)
658 }
659
660 fn persist_transition(
661 &self,
662 stored: &StoredSigner,
663 previous_state: FrostSignerSessionState,
664 previous_version: u64,
665 mutation_kind: &str,
666 fence: &StoreMutationFence,
667 ) -> Result<(), FrostStoreError> {
668 let mut connection = self.connection()?;
669 let transaction = self.begin_write(&mut connection, fence)?;
670 update_signer(&transaction, stored, previous_state, previous_version)?;
671 append_signer_commit(&transaction, self, stored, mutation_kind, fence)?;
672 self.commit_write(transaction)?;
673 self.sync_after_write(&connection)
674 }
675
676 fn burn_stored(
677 &self,
678 mut stored: StoredSigner,
679 request: &FrostSignerSessionRequest<'_>,
680 fence: &StoreMutationFence,
681 trusted_now_unix_ms: u64,
682 reason: &str,
683 ) -> Result<FrostSignerSessionRecord, FrostStoreError> {
684 validate_burn_reason(reason)?;
685 if stored.state == FrostSignerSessionState::Burned {
686 return Ok(public_record(&stored));
687 }
688 if stored.state == FrostSignerSessionState::Completed {
689 return Err(FrostStoreError::Conflict(
690 "completed signer session cannot be burned",
691 ));
692 }
693 let bound: FrostAuthorizationSlotCheckpointV1 =
694 serde_json::from_slice(&stored.bound_checkpoint_json)
695 .map_err(|error| invalid(error.to_string()))?;
696 let burn = verify_frost_authorization_slot_burn(
697 &bound,
698 request.artifact_trust,
699 trusted_now_unix_ms,
700 )
701 .map_err(transition_error)?;
702 let anchored = request
703 .slot_anchor
704 .compare_and_swap_burn(&burn)
705 .map_err(anchor_error)?;
706 verify_burned_frost_authorization_slot(
707 &bound,
708 &anchored,
709 request.artifact_trust,
710 trusted_now_unix_ms,
711 )
712 .map_err(transition_error)?;
713 let previous_state = stored.state;
714 let previous_version = stored.state_version;
715 stored.state = FrostSignerSessionState::Burned;
716 stored.state_version = previous_version
717 .checked_add(1)
718 .ok_or_else(|| invalid("signer state version overflow"))?;
719 stored.nonce = None;
720 stored.signature_share = None;
721 stored.burn_reason = Some(reason.to_string());
722 advance_record(&mut stored, fence, trusted_now_unix_ms)?;
723 self.persist_transition(
724 &stored,
725 previous_state,
726 previous_version,
727 "frost.signer.burn",
728 fence,
729 )?;
730 Ok(public_record(&stored))
731 }
732
733 fn reconcile_local_burn(
734 &self,
735 stored: &mut StoredSigner,
736 fence: &StoreMutationFence,
737 trusted_now_unix_ms: u64,
738 reason: &str,
739 ) -> Result<(), FrostStoreError> {
740 validate_burn_reason(reason)?;
741 let previous_state = stored.state;
742 let previous_version = stored.state_version;
743 stored.state = FrostSignerSessionState::Burned;
744 stored.state_version = previous_version
745 .checked_add(1)
746 .ok_or_else(|| invalid("signer state version overflow"))?;
747 stored.nonce = None;
748 stored.signature_share = None;
749 stored.burn_reason = Some(reason.to_string());
750 advance_record(stored, fence, trusted_now_unix_ms)?;
751 self.persist_transition(
752 stored,
753 previous_state,
754 previous_version,
755 "frost.signer.reconcile_burn",
756 fence,
757 )
758 }
759
760 fn reconcile_local_completion(
761 &self,
762 stored: &mut StoredSigner,
763 fence: &StoreMutationFence,
764 trusted_now_unix_ms: u64,
765 ) -> Result<(), FrostStoreError> {
766 let previous_state = stored.state;
767 let previous_version = stored.state_version;
768 stored.state = FrostSignerSessionState::Completed;
769 stored.state_version = 4;
770 stored.nonce = None;
771 advance_record(stored, fence, trusted_now_unix_ms)?;
772 self.persist_transition(
773 stored,
774 previous_state,
775 previous_version,
776 "frost.signer.reconcile_completion",
777 fence,
778 )
779 }
780}
781
782pub(super) fn verify_signer_invariants(connection: &Connection) -> Result<(), FrostStoreError> {
783 let mut statement = connection
784 .prepare(
785 "SELECT session_id, participant_id FROM frost_signer_sessions ORDER BY session_id, participant_id",
786 )
787 .map_err(super::sqlite_error)?;
788 let keys = statement
789 .query_map([], |row| {
790 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
791 })
792 .map_err(super::sqlite_error)?
793 .collect::<Result<Vec<_>, _>>()
794 .map_err(super::sqlite_error)?;
795 drop(statement);
796 for (session_id, participant_id) in keys {
797 let stored = load_signer_query(connection, &session_id, &participant_id)?
798 .ok_or_else(|| invalid("signer session disappeared during verification"))?;
799 if stored.commitment_digest != sha256_hex(&stored.commitment_bytes) {
800 return Err(invalid("FROST signer commitment digest is invalid"));
801 }
802 if signer_record_digest(&stored)? != stored.record_digest {
803 return Err(invalid("FROST signer record digest is invalid"));
804 }
805 let (sequence, digest) = connection
806 .query_row(
807 r#"
808 SELECT projection_sequence, record_digest
809 FROM frost_projection_commits
810 WHERE projection_key = ?1
811 ORDER BY projection_sequence DESC LIMIT 1
812 "#,
813 [signer_projection_key(&session_id, &participant_id)],
814 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
815 )
816 .optional()
817 .map_err(super::sqlite_error)?
818 .ok_or_else(|| invalid("signer session lacks a projection commit"))?;
819 if read_u64(sequence, "signer projection sequence")? != stored.state_version
820 || digest != stored.record_digest
821 {
822 return Err(invalid("signer session diverges from its projection head"));
823 }
824 }
825 Ok(())
826}
827
828fn validate_request(
829 request: &FrostSignerSessionRequest<'_>,
830) -> Result<ValidatedRequest, FrostStoreError> {
831 request
832 .body
833 .validate()
834 .map_err(|error| invalid(error.to_string()))?;
835 validate_identifier(request.ceremony_id, "ceremony id")?;
836 validate_identifier(request.participant_id, "participant id")?;
837 validate_identifier(request.coordinator_id, "coordinator id")?;
838 let signing_bytes = request
839 .body
840 .signing_bytes()
841 .map_err(|error| invalid(error.to_string()))?;
842 Ok(ValidatedRequest {
843 session_id: frost_authorization_session_id(request.body)
844 .map_err(|error| invalid(error.to_string()))?,
845 authorization_slot_id: frost_authorization_slot_id(request.body)
846 .map_err(|error| invalid(error.to_string()))?,
847 authorization_body_json: canonical_json_bytes(request.body)
848 .map_err(|error| invalid(error.to_string()))?,
849 signing_message_digest: sha256_hex(&signing_bytes),
850 })
851}
852
853fn verify_active_request(request: &FrostSignerSessionRequest<'_>) -> Result<(), FrostStoreError> {
854 if request.body.scope_id != request.active_roster.scope_id()
855 || request.body.key_epoch != request.active_roster.key_epoch()
856 || request.body.roster_digest != request.active_roster.roster_digest()
857 || request
858 .active_roster
859 .participant_verification_share(request.participant_id)
860 .is_none()
861 {
862 return Err(FrostStoreError::Conflict(
863 "signer request does not use the active roster",
864 ));
865 }
866 Ok(())
867}
868
869fn matches_request(
870 stored: &StoredSigner,
871 request: &FrostSignerSessionRequest<'_>,
872 validated: &ValidatedRequest,
873) -> bool {
874 stored.session_id == validated.session_id
875 && stored.authorization_slot_id == validated.authorization_slot_id
876 && stored.scope_id == request.body.scope_id
877 && stored.key_epoch == request.body.key_epoch
878 && stored.authorization_id == request.body.authorization_id
879 && stored.signing_message_digest == validated.signing_message_digest
880 && stored.roster_digest == request.body.roster_digest
881 && stored.coordinator_id == request.coordinator_id
882 && stored.resource_fence == request.body.resource_fence
883 && stored.ceremony_id == request.ceremony_id
884 && stored.authorization_body_json == validated.authorization_body_json
885 && stored.participant_id == request.participant_id
886}
887
888fn signer_nonce_aad(
889 request: &FrostSignerSessionRequest<'_>,
890 validated: &ValidatedRequest,
891 fence: &StoreMutationFence,
892) -> Result<Vec<u8>, FrostStoreError> {
893 canonical_json_bytes(&SignerNonceAad {
894 format: SIGNER_AAD_FORMAT,
895 participant_id: request.participant_id,
896 scope_id: &request.body.scope_id,
897 key_epoch: request.body.key_epoch,
898 session_id: &validated.session_id,
899 authorization_id: &request.body.authorization_id,
900 signing_message_digest: &validated.signing_message_digest,
901 coordinator_id: request.coordinator_id,
902 resource_fence: request.body.resource_fence,
903 store_uuid: &fence.store_uuid,
904 store_lease_id: &fence.lease_id,
905 store_owner_epoch: fence.owner_epoch,
906 })
907 .map_err(|error| invalid(error.to_string()))
908}
909
910fn advance_record(
911 stored: &mut StoredSigner,
912 fence: &StoreMutationFence,
913 trusted_now_unix_ms: u64,
914) -> Result<(), FrostStoreError> {
915 if trusted_now_unix_ms < stored.updated_at_unix_ms {
916 return Err(FrostStoreError::Conflict(
917 "trusted time regressed behind the signer session",
918 ));
919 }
920 stored.source_fence = fence.clone();
921 stored.updated_at_unix_ms = trusted_now_unix_ms;
922 stored.record_digest = signer_record_digest(stored)?;
923 Ok(())
924}
925
926fn public_record(stored: &StoredSigner) -> FrostSignerSessionRecord {
927 FrostSignerSessionRecord {
928 session_id: stored.session_id.clone(),
929 participant_id: stored.participant_id.clone(),
930 authorization_slot_id: stored.authorization_slot_id.clone(),
931 state: stored.state,
932 state_version: stored.state_version,
933 }
934}
935
936fn public_commitment(stored: &StoredSigner) -> FrostSignerCommitment {
937 FrostSignerCommitment {
938 session_id: stored.session_id.clone(),
939 participant_id: stored.participant_id.clone(),
940 signer_identifier: stored.signer_identifier.clone(),
941 commitment_bytes: stored.commitment_bytes.clone(),
942 }
943}
944
945fn signer_projection_key(session_id: &str, participant_id: &str) -> String {
946 format!("signer/{session_id}/{participant_id}")
947}
948
949fn verify_custody(stored: &StoredSigner, custody: &FrostCustodyKey) -> Result<(), FrostStoreError> {
950 if stored.custody_generation != custody.generation() {
951 return Err(FrostStoreError::Custody(
952 "custody generation does not match the signer session",
953 ));
954 }
955 Ok(())
956}
957
958fn validate_identifier(value: &str, _field: &'static str) -> Result<(), FrostStoreError> {
959 if value.is_empty()
960 || value.len() > 256
961 || value.trim() != value
962 || !value.bytes().all(|byte| byte.is_ascii_graphic())
963 {
964 return Err(FrostStoreError::Conflict("signer identifier is invalid"));
965 }
966 Ok(())
967}
968
969fn validate_burn_reason(reason: &str) -> Result<(), FrostStoreError> {
970 if reason.is_empty()
971 || reason.len() > MAX_BURN_REASON_BYTES
972 || reason.trim() != reason
973 || reason.bytes().any(|byte| byte.is_ascii_control())
974 {
975 return Err(FrostStoreError::Conflict("signer burn reason is invalid"));
976 }
977 Ok(())
978}
979
980fn validate_time(value: u64) -> Result<(), FrostStoreError> {
981 if value == 0 {
982 return Err(FrostStoreError::Conflict(
983 "trusted signer time must be non-zero",
984 ));
985 }
986 Ok(())
987}
988
989fn transition_error(error: impl std::fmt::Display) -> FrostStoreError {
990 invalid(error.to_string())
991}
992
993fn anchor_error(error: impl std::fmt::Display) -> FrostStoreError {
994 FrostStoreError::Unavailable(error.to_string())
995}
996
997fn invalid(detail: impl Into<String>) -> FrostStoreError {
998 FrostStoreError::InvalidState(detail.into())
999}
1000
1001fn sqlite_u64(value: u64, field: &'static str) -> Result<i64, FrostStoreError> {
1002 i64::try_from(value).map_err(|_| invalid(format!("{field} exceeds SQLite range")))
1003}
1004
1005fn sqlite_read_u64(row: &Row<'_>, index: usize) -> rusqlite::Result<u64> {
1006 let value = row.get::<_, i64>(index)?;
1007 u64::try_from(value).map_err(|error| {
1008 rusqlite::Error::FromSqlConversionFailure(
1009 index,
1010 rusqlite::types::Type::Integer,
1011 Box::new(error),
1012 )
1013 })
1014}
1015
1016fn read_u64(value: i64, field: &'static str) -> Result<u64, FrostStoreError> {
1017 u64::try_from(value).map_err(|_| invalid(format!("{field} is negative")))
1018}
1019
1020fn to_sql_conversion(error: FrostStoreError) -> rusqlite::Error {
1021 rusqlite::Error::FromSqlConversionFailure(16, rusqlite::types::Type::Text, Box::new(error))
1022}