120 lines
3.9 KiB
Rust
120 lines
3.9 KiB
Rust
|
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(())
|
||
|
}
|