Skip to main content

chio_store_sqlite/frost_store/
signer.rs

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}