use chio_core_types::canonical::canonical_json_bytes;
use chio_core_types::crypto::{Ed25519Backend, Keypair, PublicKey, Signature, SigningBackend};
use chio_core_types::receipt::ChioReceipt;
use serde::{Deserialize, Serialize};
pub const BILATERAL_COSIGNING_SCHEMA: &str = "chio.federation-bilateral-cosigning.v1";
pub const BILATERAL_DUAL_RECEIPT_SCHEMA: &str = "chio.federation-dual-signed-receipt.v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CoSigningBody {
pub schema: String,
pub receipt_canonical_json: String,
pub org_a_kernel_id: String,
pub org_b_kernel_id: String,
}
impl CoSigningBody {
pub fn from_receipt(
receipt: &ChioReceipt,
org_a_kernel_id: &str,
org_b_kernel_id: &str,
) -> Result<Self, BilateralCoSigningError> {
let bytes = canonical_json_bytes(receipt)
.map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))?;
let receipt_canonical_json = String::from_utf8(bytes)
.map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))?;
Ok(Self {
schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
receipt_canonical_json,
org_a_kernel_id: org_a_kernel_id.to_string(),
org_b_kernel_id: org_b_kernel_id.to_string(),
})
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, BilateralCoSigningError> {
canonical_json_bytes(self)
.map_err(|e| BilateralCoSigningError::CanonicalJson(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DualSignedReceipt {
pub schema: String,
pub body: ChioReceipt,
pub org_a_kernel_id: String,
pub org_b_kernel_id: String,
pub org_a_signature: Signature,
pub org_b_signature: Signature,
}
impl DualSignedReceipt {
pub fn verify(
&self,
org_a_public_key: &PublicKey,
org_b_public_key: &PublicKey,
) -> Result<(), BilateralCoSigningError> {
let body =
CoSigningBody::from_receipt(&self.body, &self.org_a_kernel_id, &self.org_b_kernel_id)?;
let bytes = body.canonical_bytes()?;
if !org_a_public_key.verify(&bytes, &self.org_a_signature) {
return Err(BilateralCoSigningError::OrgASignatureInvalid);
}
if !org_b_public_key.verify(&bytes, &self.org_b_signature) {
return Err(BilateralCoSigningError::OrgBSignatureInvalid);
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CoSigningRequest {
pub schema: String,
pub body: ChioReceipt,
pub org_a_kernel_id: String,
pub org_b_kernel_id: String,
pub org_b_signature: Signature,
}
impl CoSigningRequest {
pub fn new(
body: ChioReceipt,
org_a_kernel_id: String,
org_b_kernel_id: String,
org_b_signature: Signature,
) -> Self {
Self {
schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
body,
org_a_kernel_id,
org_b_kernel_id,
org_b_signature,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CoSigningResponse {
pub schema: String,
pub org_a_signature: Signature,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum BilateralCoSigningError {
#[error("canonical JSON encoding failed: {0}")]
CanonicalJson(String),
#[error("origin (Org A) signature failed verification")]
OrgASignatureInvalid,
#[error("tool-host (Org B) signature failed verification")]
OrgBSignatureInvalid,
#[error("remote peer {0} is not a trusted federation peer")]
UnknownPeer(String),
#[error("remote peer {0} has exceeded its rotation window and must re-handshake")]
PeerExpired(String),
#[error("co-signing transport failed: {0}")]
TransportFailure(String),
#[error("co-signing request rejected by peer: {0}")]
PeerRejected(String),
#[error("receipt body mismatch between request and signed body")]
ReceiptMismatch,
}
pub trait BilateralCoSigningProtocol: Send + Sync {
fn request_cosignature(
&self,
request: &CoSigningRequest,
) -> Result<CoSigningResponse, BilateralCoSigningError>;
}
pub struct InProcessCoSigner {
origin_kernel_id: String,
origin_keypair: Keypair,
tool_host_public_key: PublicKey,
}
impl core::fmt::Debug for InProcessCoSigner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("InProcessCoSigner")
.field("origin_kernel_id", &self.origin_kernel_id)
.finish_non_exhaustive()
}
}
impl InProcessCoSigner {
pub fn new(
origin_kernel_id: impl Into<String>,
origin_keypair: Keypair,
tool_host_public_key: PublicKey,
) -> Self {
Self {
origin_kernel_id: origin_kernel_id.into(),
origin_keypair,
tool_host_public_key,
}
}
pub fn origin_kernel_id(&self) -> &str {
&self.origin_kernel_id
}
pub fn origin_public_key(&self) -> PublicKey {
self.origin_keypair.public_key()
}
}
impl BilateralCoSigningProtocol for InProcessCoSigner {
fn request_cosignature(
&self,
request: &CoSigningRequest,
) -> Result<CoSigningResponse, BilateralCoSigningError> {
if request.org_a_kernel_id != self.origin_kernel_id {
return Err(BilateralCoSigningError::UnknownPeer(
request.org_a_kernel_id.clone(),
));
}
let body = CoSigningBody::from_receipt(
&request.body,
&request.org_a_kernel_id,
&request.org_b_kernel_id,
)?;
let bytes = body.canonical_bytes()?;
if !self
.tool_host_public_key
.verify(&bytes, &request.org_b_signature)
{
return Err(BilateralCoSigningError::OrgBSignatureInvalid);
}
let backend = Ed25519Backend::new(self.origin_keypair.clone());
let signature = backend
.sign_bytes(&bytes)
.map_err(|e| BilateralCoSigningError::TransportFailure(e.to_string()))?;
Ok(CoSigningResponse {
schema: BILATERAL_COSIGNING_SCHEMA.to_string(),
org_a_signature: signature,
})
}
}
pub fn co_sign_with_origin(
origin_kernel_id: &str,
origin_public_key: &PublicKey,
tool_host_kernel_id: &str,
tool_host_keypair: &Keypair,
receipt: ChioReceipt,
cosigner: &dyn BilateralCoSigningProtocol,
) -> Result<DualSignedReceipt, BilateralCoSigningError> {
let body = CoSigningBody::from_receipt(&receipt, origin_kernel_id, tool_host_kernel_id)?;
let bytes = body.canonical_bytes()?;
let backend = Ed25519Backend::new(tool_host_keypair.clone());
let org_b_signature = backend
.sign_bytes(&bytes)
.map_err(|e| BilateralCoSigningError::TransportFailure(e.to_string()))?;
let request = CoSigningRequest::new(
receipt.clone(),
origin_kernel_id.to_string(),
tool_host_kernel_id.to_string(),
org_b_signature.clone(),
);
let response = cosigner.request_cosignature(&request)?;
if !origin_public_key.verify(&bytes, &response.org_a_signature) {
return Err(BilateralCoSigningError::OrgASignatureInvalid);
}
let dual = DualSignedReceipt {
schema: BILATERAL_DUAL_RECEIPT_SCHEMA.to_string(),
body: receipt,
org_a_kernel_id: origin_kernel_id.to_string(),
org_b_kernel_id: tool_host_kernel_id.to_string(),
org_a_signature: response.org_a_signature,
org_b_signature,
};
dual.verify(origin_public_key, &tool_host_keypair.public_key())?;
Ok(dual)
}