1use chio_core_types::canonical::canonical_json_bytes;
28use chio_core_types::crypto::{Ed25519Backend, Keypair, PublicKey, Signature, SigningBackend};
29use chio_core_types::receipt::ChioReceipt;
30use serde::{Deserialize, Serialize};
31
32pub const BILATERAL_COSIGNING_SCHEMA: &str = "chio.federation-bilateral-cosigning.v1";
33pub const BILATERAL_DUAL_RECEIPT_SCHEMA: &str = "chio.federation-dual-signed-receipt.v1";
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct CoSigningBody {
42 pub schema: String,
43 pub receipt_canonical_json: String,
48 pub org_a_kernel_id: String,
49 pub org_b_kernel_id: String,
50}
51
52impl CoSigningBody {
53 pub fn from_receipt(
57 receipt: &ChioReceipt,
58 org_a_kernel_id: &str,
59 org_b_kernel_id: &str,
60 ) -> Result<Self, BilateralCoSigningError> {
61 let bytes = canonical_json_bytes(receipt)
62 .map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))?;
63 let receipt_canonical_json = String::from_utf8(bytes)
64 .map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))?;
65 Ok(Self {
66 schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
67 receipt_canonical_json,
68 org_a_kernel_id: org_a_kernel_id.to_string(),
69 org_b_kernel_id: org_b_kernel_id.to_string(),
70 })
71 }
72
73 pub fn canonical_bytes(&self) -> Result<Vec<u8>, BilateralCoSigningError> {
74 canonical_json_bytes(self)
75 .map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct DualSignedReceipt {
94 pub schema: String,
95 pub body: ChioReceipt,
96 pub org_a_kernel_id: String,
97 pub org_b_kernel_id: String,
98 pub org_a_signature: Signature,
99 pub org_b_signature: Signature,
100}
101
102impl DualSignedReceipt {
103 pub fn verify(
109 &self,
110 org_a_public_key: &PublicKey,
111 org_b_public_key: &PublicKey,
112 ) -> Result<(), BilateralCoSigningError> {
113 let body =
114 CoSigningBody::from_receipt(&self.body, &self.org_a_kernel_id, &self.org_b_kernel_id)?;
115 let bytes = body.canonical_bytes()?;
116
117 if !org_a_public_key.verify(&bytes, &self.org_a_signature) {
118 return Err(BilateralCoSigningError::OrgASignatureInvalid);
119 }
120 if !org_b_public_key.verify(&bytes, &self.org_b_signature) {
121 return Err(BilateralCoSigningError::OrgBSignatureInvalid);
122 }
123 Ok(())
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase", deny_unknown_fields)]
132pub struct CoSigningRequest {
133 pub schema: String,
134 pub body: ChioReceipt,
135 pub org_a_kernel_id: String,
136 pub org_b_kernel_id: String,
137 pub org_b_signature: Signature,
140}
141
142impl CoSigningRequest {
143 pub fn new(
144 body: ChioReceipt,
145 org_a_kernel_id: String,
146 org_b_kernel_id: String,
147 org_b_signature: Signature,
148 ) -> Self {
149 Self {
150 schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
151 body,
152 org_a_kernel_id,
153 org_b_kernel_id,
154 org_b_signature,
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct CoSigningResponse {
163 pub schema: String,
164 pub org_a_signature: Signature,
165}
166
167#[derive(Debug, thiserror::Error, PartialEq, Eq)]
171pub enum BilateralCoSigningError {
172 #[error("canonical JSON encoding failed: {0}")]
173 CanonicalJson(String),
174
175 #[error("origin (Org A) signature failed verification")]
176 OrgASignatureInvalid,
177
178 #[error("tool-host (Org B) signature failed verification")]
179 OrgBSignatureInvalid,
180
181 #[error("remote peer {0} is not a trusted federation peer")]
182 UnknownPeer(String),
183
184 #[error("remote peer {0} has exceeded its rotation window and must re-handshake")]
185 PeerExpired(String),
186
187 #[error("co-signing transport failed: {0}")]
188 TransportFailure(String),
189
190 #[error("co-signing request rejected by peer: {0}")]
191 PeerRejected(String),
192
193 #[error("receipt body mismatch between request and signed body")]
194 ReceiptMismatch,
195}
196
197pub trait BilateralCoSigningProtocol: Send + Sync {
201 fn request_cosignature(
205 &self,
206 request: &CoSigningRequest,
207 ) -> Result<CoSigningResponse, BilateralCoSigningError>;
208}
209
210pub struct InProcessCoSigner {
217 origin_kernel_id: String,
218 origin_keypair: Keypair,
219 tool_host_public_key: PublicKey,
223}
224
225impl core::fmt::Debug for InProcessCoSigner {
226 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
227 f.debug_struct("InProcessCoSigner")
228 .field("origin_kernel_id", &self.origin_kernel_id)
229 .finish_non_exhaustive()
230 }
231}
232
233impl InProcessCoSigner {
234 pub fn new(
235 origin_kernel_id: impl Into<String>,
236 origin_keypair: Keypair,
237 tool_host_public_key: PublicKey,
238 ) -> Self {
239 Self {
240 origin_kernel_id: origin_kernel_id.into(),
241 origin_keypair,
242 tool_host_public_key,
243 }
244 }
245
246 pub fn origin_kernel_id(&self) -> &str {
247 &self.origin_kernel_id
248 }
249
250 pub fn origin_public_key(&self) -> PublicKey {
251 self.origin_keypair.public_key()
252 }
253}
254
255impl BilateralCoSigningProtocol for InProcessCoSigner {
256 fn request_cosignature(
257 &self,
258 request: &CoSigningRequest,
259 ) -> Result<CoSigningResponse, BilateralCoSigningError> {
260 if request.org_a_kernel_id != self.origin_kernel_id {
261 return Err(BilateralCoSigningError::UnknownPeer(
262 request.org_a_kernel_id.clone(),
263 ));
264 }
265 let body = CoSigningBody::from_receipt(
266 &request.body,
267 &request.org_a_kernel_id,
268 &request.org_b_kernel_id,
269 )?;
270 let bytes = body.canonical_bytes()?;
271
272 if !self
273 .tool_host_public_key
274 .verify(&bytes, &request.org_b_signature)
275 {
276 return Err(BilateralCoSigningError::OrgBSignatureInvalid);
277 }
278
279 let backend = Ed25519Backend::new(self.origin_keypair.clone());
280 let signature = backend
281 .sign_bytes(&bytes)
282 .map_err(|e| BilateralCoSigningError::TransportFailure(e.to_string()))?;
283 Ok(CoSigningResponse {
284 schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
285 org_a_signature: signature,
286 })
287 }
288}
289
290pub fn co_sign_with_origin(
294 origin_kernel_id: &str,
295 origin_public_key: &PublicKey,
296 tool_host_kernel_id: &str,
297 tool_host_keypair: &Keypair,
298 receipt: ChioReceipt,
299 cosigner: &dyn BilateralCoSigningProtocol,
300) -> Result<DualSignedReceipt, BilateralCoSigningError> {
301 let body = CoSigningBody::from_receipt(&receipt, origin_kernel_id, tool_host_kernel_id)?;
302 let bytes = body.canonical_bytes()?;
303
304 let backend = Ed25519Backend::new(tool_host_keypair.clone());
305 let org_b_signature = backend
306 .sign_bytes(&bytes)
307 .map_err(|e| BilateralCoSigningError::TransportFailure(e.to_string()))?;
308
309 let request = CoSigningRequest::new(
310 receipt.clone(),
311 origin_kernel_id.to_string(),
312 tool_host_kernel_id.to_string(),
313 org_b_signature.clone(),
314 );
315 let response = cosigner.request_cosignature(&request)?;
316
317 if !origin_public_key.verify(&bytes, &response.org_a_signature) {
318 return Err(BilateralCoSigningError::OrgASignatureInvalid);
319 }
320
321 let dual = DualSignedReceipt {
322 schema: BILATERAL_DUAL_RECEIPT_SCHEMA.to_string(),
323 body: receipt,
324 org_a_kernel_id: origin_kernel_id.to_string(),
325 org_b_kernel_id: tool_host_kernel_id.to_string(),
326 org_a_signature: response.org_a_signature,
327 org_b_signature,
328 };
329 dual.verify(origin_public_key, &tool_host_keypair.public_key())?;
333 Ok(dual)
334}