relay/src/repo/mod.rs

120 lines
3.9 KiB
Rust
Raw Normal View History

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(())
}