use std::collections::BTreeMap; use anyhow::{bail, Context, Result}; use atrium_api::com::atproto::sync::subscribe_repos::CommitData; use bytes::Bytes; use ecdsa::signature::Verifier; use ipld_core::cid::Cid; use iroh_car::CarReader; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::user::User; #[derive(Debug, Serialize, Deserialize)] pub struct UnsignedCommitNode { pub did: String, pub version: u8, pub prev: Option<Cid>, pub rev: String, pub data: Cid, } #[derive(Debug, Serialize, Deserialize)] pub struct SignedCommitNode { #[serde(flatten)] pub node: UnsignedCommitNode, pub sig: Bytes, } pub async fn validate_commit(user: &User, commit: &CommitData) -> Result<()> { let mut car_reader = CarReader::new(commit.blocks.as_slice()).await?; let car_header = car_reader.header().clone(); let mut blocks = BTreeMap::new(); while let Some((cid, cbor)) = car_reader.next_block().await? { let (hash_type, hash_digest, hash_len) = cid.hash().into_inner(); if hash_type != 0x12 || hash_len != 32 { bail!( "unexpected cid type {:?} (expected sha-256 [id 18] with 32 bytes)", cid ) } let cid_display = cid.to_string_of_base(multibase::Base::Base32Lower)?; let record_hash = Sha256::digest(&cbor); if &hash_digest[..32] != record_hash.as_slice() { bail!("cid doesn't match cbor hash: {}", &cid_display) } blocks.insert(cid, cbor); } /* let block_cids = blocks .keys() .filter_map(|cid| cid.to_string_of_base(multibase::Base::Base32Lower).ok()) .collect::<Vec<_>>(); dbg!(block_cids); let root_cids = car_header .roots() .iter() .filter_map(|cid| cid.to_string_of_base(multibase::Base::Base32Lower).ok()) .collect::<Vec<_>>(); dbg!(root_cids); */ let signing_key = user .signing_key() .context("couldn't find signing key for user")?; let (_, signing_key) = multibase::decode(signing_key).unwrap(); for root in car_header.roots() { let cid_display = root.to_string_of_base(multibase::Base::Base32Lower)?; let Some(block) = blocks.get(root) else { bail!("block did not exist: {}", &cid_display) }; let commit: SignedCommitNode = serde_ipld_dagcbor::from_slice(block) .context("couldn't deserialize signed commit object")?; if commit.node.did != user.did { bail!("did in car doesn't match repo did {}", &cid_display) } let unsigned_data = serde_ipld_dagcbor::to_vec(&commit.node)?; if commit.sig.len() != 64 { bail!( "unexpected signature length {} (expected 64)", commit.sig.len() ); } let r: [u8; 32] = commit.sig[..32].try_into().unwrap(); let s: [u8; 32] = commit.sig[32..].try_into().unwrap(); match signing_key[..2] { [0xe7, 0x01] => { let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&signing_key[2..]).unwrap(); let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap(); key.verify(&unsigned_data, &sig) .context("failed to verify k256") } [0x80, 0x24] => { let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&signing_key[2..]).unwrap(); let sig = p256::ecdsa::Signature::from_scalars(r, s).unwrap(); key.verify(&unsigned_data, &sig) .context("failed to verify p256") } _ => Err(anyhow::anyhow!( "unknown signing key format {:?}", &signing_key[..2] )), }?; // TODO: dfs for cid from commit.node.data, error if cid is not in any signed root } Ok(()) }