//! Commit structures and signature verification for AT Protocol repositories. //! //! This module provides repository commit object handling with signature support. pub mod firehose; pub mod proof; pub(crate) mod serde_bytes_helper; use crate::error::{CommitError, Result}; use bytes::Bytes; use cid::Cid as IpldCid; use jacquard_common::types::crypto::PublicKey; use jacquard_common::types::string::Did; use jacquard_common::types::tid::Tid; use jacquard_common::{BosStr, IntoStatic}; /// Repository commit object /// /// This structure represents a signed commit in an AT Protocol repository. /// Stored as a block in CAR files, identified by its CID. /// /// **Version compatibility**: v2 and v3 commits differ only in how `prev` is /// serialized (v2 uses it, v3 must include it even if null). This struct /// handles both by always including `prev` in serialization. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Commit { /// Repository DID pub did: Did, /// Commit version (2 or 3) pub version: i64, /// MST root CID pub data: IpldCid, /// Revision TID pub rev: Tid, /// Previous commit CID (None for initial commit) pub prev: Option, /// Signature bytes #[serde(with = "serde_bytes_helper")] pub sig: Bytes, } /// Unsigned commit object. /// Explicitly a separate struct minus the sig field to match deserialization/signatures /// from other implementations. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct UnsignedCommit { /// Repository DID pub did: Did, /// Commit version (2 or 3) pub version: i64, /// MST root CID pub data: IpldCid, /// Revision TID pub rev: Tid, /// Previous commit CID (None for initial commit) pub prev: Option, } impl UnsignedCommit { /// Create new unsigned commit (version = 3, sig empty) pub fn new_unsigned( did: Did, data: IpldCid, rev: Tid, prev: Option, ) -> UnsignedCommit { Self { did, version: 3, data, rev, prev, } } /// Get unsigned commit bytes (for signing/verification) pub(super) fn unsigned_bytes(&self) -> Result> { // Serialize without signature field serde_ipld_dagcbor::to_vec(self) .map_err(|e| crate::error::CommitError::Serialization(Box::new(e)).into()) } /// Sign this commit with a key pub fn sign(self, key: &impl SigningKey) -> Result> { let unsigned = self.unsigned_bytes()?; let sig = key.sign_bytes(&unsigned)?; Ok(Commit { did: self.did, version: self.version, data: self.data, rev: self.rev, prev: self.prev, sig, }) } /// Get the repository DID pub fn did(&self) -> &Did { &self.did } /// Get the MST root CID pub fn data(&self) -> &IpldCid { &self.data } /// Get the revision TID pub fn rev(&self) -> &Tid { &self.rev } /// Get the previous commit CID pub fn prev(&self) -> Option<&IpldCid> { self.prev.as_ref() } } impl Commit { /// Create new unsigned commit (version = 3, sig empty) pub fn new_unsigned( did: Did, data: IpldCid, rev: Tid, prev: Option, ) -> UnsignedCommit { UnsignedCommit { did, version: 3, data, rev, prev, } } /// Get unsigned commit bytes (for signing/verification) pub(super) fn unsigned_bytes(&self) -> Result> { // Serialize without signature field let unsigned = UnsignedCommit { did: self.did.clone(), version: self.version, data: self.data.clone(), rev: self.rev.clone(), prev: self.prev.clone(), }; serde_ipld_dagcbor::to_vec(&unsigned) .map_err(|e| crate::error::CommitError::Serialization(Box::new(e)).into()) } /// Get the repository DID pub fn did(&self) -> &Did { &self.did } /// Get the MST root CID pub fn data(&self) -> &IpldCid { &self.data } /// Get the revision TID pub fn rev(&self) -> &Tid { &self.rev } /// Get the previous commit CID pub fn prev(&self) -> Option<&IpldCid> { self.prev.as_ref() } /// Get the signature bytes pub fn sig(&self) -> &Bytes { &self.sig } /// Serialize to DAG-CBOR pub fn to_cbor(&self) -> Result> { serde_ipld_dagcbor::to_vec(self).map_err(|e| CommitError::Serialization(Box::new(e)).into()) } /// Deserialize from DAG-CBOR pub fn from_cbor<'a>(data: &'a [u8]) -> Result where S: serde::Deserialize<'a>, { serde_ipld_dagcbor::from_slice(data) .map_err(|e| CommitError::Serialization(Box::new(e)).into()) } /// Compute CID of this commit pub fn to_cid(&self) -> Result { let cbor = self.to_cbor()?; crate::mst::util::compute_cid(&cbor) } /// Verify signature against a public key from a DID document. /// /// The key type is inferred from the PublicKey codec. pub fn verify(&self, pubkey: &PublicKey) -> std::result::Result<(), CommitError> { let unsigned = self .unsigned_bytes() .map_err(|e| CommitError::Serialization(e.into()))?; let signature = self.sig(); use jacquard_common::types::crypto::KeyCodec; match pubkey.codec { KeyCodec::Ed25519 => { let vk = pubkey .to_ed25519() .map_err(|e| CommitError::InvalidKey(e.to_string()))?; let sig = ed25519_dalek::Signature::from_slice(signature.as_ref()) .map_err(|e| CommitError::InvalidSignature(e.to_string()))?; vk.verify_strict(&unsigned, &sig) .map_err(|_| CommitError::SignatureVerificationFailed)?; } KeyCodec::Secp256k1 => { use k256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = pubkey .to_k256() .map_err(|e| CommitError::InvalidKey(e.to_string()))?; let verifying_key = VerifyingKey::from(&vk); let sig = Signature::from_slice(signature.as_ref()) .map_err(|e| CommitError::InvalidSignature(e.to_string()))?; verifying_key .verify(&unsigned, &sig) .map_err(|_| CommitError::SignatureVerificationFailed)?; } KeyCodec::P256 => { use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = pubkey .to_p256() .map_err(|e| CommitError::InvalidKey(e.to_string()))?; let verifying_key = VerifyingKey::from(&vk); let sig = Signature::from_slice(signature.as_ref()) .map_err(|e| CommitError::InvalidSignature(e.to_string()))?; verifying_key .verify(&unsigned, &sig) .map_err(|_| CommitError::SignatureVerificationFailed)?; } KeyCodec::Unknown(code) => { return Err(CommitError::UnsupportedKeyType(code)); } } Ok(()) } } impl IntoStatic for Commit where S: BosStr + IntoStatic, S::Output: BosStr, { type Output = Commit; fn into_static(self) -> Self::Output { Commit { did: self.did.into_static(), version: self.version, data: self.data, rev: self.rev, prev: self.prev, sig: self.sig, } } } /// Trait for signing keys. /// /// Implemented for ed25519_dalek::SigningKey, k256::ecdsa::SigningKey, and p256::ecdsa::SigningKey. pub trait SigningKey { /// Sign the given data and return signature as Bytes fn sign_bytes(&self, data: &[u8]) -> Result; /// Get the public key bytes fn public_key(&self) -> Vec; } // Ed25519 implementation impl SigningKey for ed25519_dalek::SigningKey { fn sign_bytes(&self, data: &[u8]) -> Result { use ed25519_dalek::Signer; let sig = Signer::sign(self, data); Ok(Bytes::copy_from_slice(&sig.to_bytes())) } fn public_key(&self) -> Vec { self.verifying_key().to_bytes().to_vec() } } // K-256 (secp256k1) implementation impl SigningKey for k256::ecdsa::SigningKey { fn sign_bytes(&self, data: &[u8]) -> Result { use k256::ecdsa::signature::Signer; let sig: k256::ecdsa::Signature = Signer::sign(self, data); Ok(Bytes::copy_from_slice(&sig.to_bytes())) } fn public_key(&self) -> Vec { self.verifying_key() .to_encoded_point(true) .as_bytes() .to_vec() } } // P-256 implementation impl SigningKey for p256::ecdsa::SigningKey { fn sign_bytes(&self, data: &[u8]) -> Result { use p256::ecdsa::signature::Signer; let sig: p256::ecdsa::Signature = Signer::sign(self, data); Ok(Bytes::copy_from_slice(&sig.to_bytes())) } fn public_key(&self) -> Vec { self.verifying_key() .to_encoded_point(true) .as_bytes() .to_vec() } }