1use std::collections::BTreeMap;
2use std::sync::Mutex;
3
4use frost_ed25519 as frost;
5use rand::rngs::OsRng;
6use rand::RngCore;
7
8use crate::rpc::{NonceSlotId, QuorumRpcMessage, SignCertRequestPayload, SignCertResponsePayload};
9
10#[derive(Clone, Debug)]
12pub struct SignerPolicy {
13 pub max_ttl_secs: u64,
14 pub admin_provisioning_pubkey: Option<ed25519_dalek::VerifyingKey>,
15 pub ipv4_network_mask: Option<(u32, u32)>,
16}
17
18impl Default for SignerPolicy {
19 fn default() -> Self {
20 Self {
21 max_ttl_secs: 7 * 86_400, admin_provisioning_pubkey: None,
23 ipv4_network_mask: None,
24 }
25 }
26}
27
28pub struct EphemeralNonceVault {
31 boot_epoch_id: u128,
32 next_gen_idx: u64,
33 active_nonces: BTreeMap<NonceSlotId, frost::round1::SigningNonces>,
34}
35
36impl EphemeralNonceVault {
37 pub fn new() -> Self {
39 let mut bytes = [0u8; 16];
40 OsRng.fill_bytes(&mut bytes);
41 Self {
42 boot_epoch_id: u128::from_le_bytes(bytes),
43 next_gen_idx: 1,
44 active_nonces: BTreeMap::new(),
45 }
46 }
47
48 pub fn boot_epoch_id(&self) -> u128 {
50 self.boot_epoch_id
51 }
52
53 pub fn preprocess_batch(
55 &mut self,
56 signing_share: &frost::keys::SigningShare,
57 count: usize,
58 ) -> Vec<(NonceSlotId, frost::round1::SigningCommitments)> {
59 let mut out = Vec::with_capacity(count);
60 let mut rng = OsRng;
61 for _ in 0..count {
62 let seq = self.next_gen_idx;
63 self.next_gen_idx = self
64 .next_gen_idx
65 .checked_add(1)
66 .expect("nonce counter overflow");
67 let (nonces, commitments) = frost::round1::commit(signing_share, &mut rng);
68 let slot = NonceSlotId {
69 boot_epoch_id: self.boot_epoch_id,
70 sequence_idx: seq,
71 };
72 self.active_nonces.insert(slot, nonces);
73 out.push((slot, commitments));
74 }
75 out
76 }
77
78 pub fn take_once(&mut self, slot: &NonceSlotId) -> Option<frost::round1::SigningNonces> {
81 if slot.boot_epoch_id != self.boot_epoch_id {
82 return None;
83 }
84 self.active_nonces.remove(slot)
85 }
86
87 pub fn len(&self) -> usize {
89 self.active_nonces.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.active_nonces.is_empty()
95 }
96}
97
98impl Default for EphemeralNonceVault {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104pub struct SignerState {
106 pub key_package: frost::keys::KeyPackage,
107 pub pubkey_package: frost::keys::PublicKeyPackage,
108 pub policy: SignerPolicy,
109 pub vault: Mutex<EphemeralNonceVault>,
110}
111
112impl SignerState {
113 pub fn new(
114 key_package: frost::keys::KeyPackage,
115 pubkey_package: frost::keys::PublicKeyPackage,
116 policy: SignerPolicy,
117 ) -> Self {
118 Self {
119 key_package,
120 pubkey_package,
121 policy,
122 vault: Mutex::new(EphemeralNonceVault::new()),
123 }
124 }
125
126 pub fn handle_sign_request(&self, req: &SignCertRequestPayload) -> QuorumRpcMessage {
129 let signer_id = *self.key_package.identifier();
130 let session_id = req.session_id;
131
132 let expected_msg = req.cert_tbs.to_canonical_bytes();
134 if req.signing_package.message() != expected_msg.as_slice() {
135 return QuorumRpcMessage::SignCertReject {
136 session_id,
137 signer_id,
138 reason: "SigningPackage payload mismatch with CertTbs".into(),
139 };
140 }
141
142 if req.cert_tbs.not_after <= req.cert_tbs.not_before
144 || (req.cert_tbs.not_after - req.cert_tbs.not_before) > self.policy.max_ttl_secs
145 {
146 return QuorumRpcMessage::SignCertReject {
147 session_id,
148 signer_id,
149 reason: "Certificate TTL exceeds signer policy maximum".into(),
150 };
151 }
152
153 let (nonces, replenish) = {
155 let mut vault = match self.vault.lock() {
156 Ok(guard) => guard,
157 Err(poisoned) => poisoned.into_inner(),
158 };
159 let Some(n) = vault.take_once(&req.slot_id) else {
160 return QuorumRpcMessage::SignCertReject {
161 session_id,
162 signer_id,
163 reason: "NonceSlotId not found, already consumed, or stale boot epoch".into(),
164 };
165 };
166 let rep = vault
167 .preprocess_batch(self.key_package.signing_share(), 1)
168 .pop();
169 (n, rep)
170 };
171
172 match frost::round2::sign(&req.signing_package, &nonces, &self.key_package) {
174 Ok(signature_share) => {
175 QuorumRpcMessage::SignCertResponse(Box::new(SignCertResponsePayload {
176 session_id,
177 signer_id,
178 signature_share,
179 replenish_commitment: replenish,
180 }))
181 }
182 Err(e) => QuorumRpcMessage::SignCertReject {
183 session_id,
184 signer_id,
185 reason: format!("FROST round2 sign error: {e}"),
186 },
187 }
188 }
189}