forked from
lewis.moe/bspds-sandbox
PDS software with bells & whistles you didn’t even know you needed. will move this to its own account when ready.
1use bytes::Bytes;
2use cid::Cid;
3use std::collections::HashMap;
4mod common;
5
6#[tokio::test]
7#[ignore = "depends on external live server state; run manually with --ignored"]
8async fn test_verify_live_commit() {
9 let client = reqwest::Client::new();
10 let did = "did:plc:zp3oggo2mikqntmhrc4scby4";
11 let resp = client
12 .get(format!(
13 "https://testpds.wizardry.systems/xrpc/com.atproto.sync.getRepo?did={}",
14 did
15 ))
16 .send()
17 .await
18 .expect("Failed to fetch repo");
19 assert!(
20 resp.status().is_success(),
21 "getRepo failed: {}",
22 resp.status()
23 );
24 let car_bytes = resp.bytes().await.expect("Failed to read body");
25 println!("CAR bytes: {} bytes", car_bytes.len());
26 let mut cursor = std::io::Cursor::new(&car_bytes[..]);
27 let (roots, blocks) = parse_car(&mut cursor).expect("Failed to parse CAR");
28 println!("CAR roots: {:?}", roots);
29 println!("CAR blocks: {}", blocks.len());
30 assert!(!roots.is_empty(), "No roots in CAR");
31 let root_cid = roots[0];
32 let root_block = blocks.get(&root_cid).expect("Root block not found");
33 let commit =
34 jacquard_repo::commit::Commit::from_cbor(root_block).expect("Failed to parse commit");
35 println!("Commit DID: {}", commit.did().as_str());
36 println!("Commit rev: {}", commit.rev());
37 println!("Commit prev: {:?}", commit.prev());
38 println!("Commit sig length: {} bytes", commit.sig().len());
39 let resp = client
40 .get(format!("https://plc.directory/{}", did))
41 .send()
42 .await
43 .expect("Failed to fetch DID doc");
44 let did_doc_text = resp.text().await.expect("Failed to read body");
45 println!("DID doc: {}", did_doc_text);
46 let did_doc: jacquard::common::types::did_doc::DidDocument<'_> =
47 serde_json::from_str(&did_doc_text).expect("Failed to parse DID doc");
48 let pubkey = did_doc
49 .atproto_public_key()
50 .expect("Failed to get public key")
51 .expect("No public key");
52 println!("Public key codec: {:?}", pubkey.codec);
53 println!("Public key bytes: {} bytes", pubkey.bytes.len());
54 match commit.verify(&pubkey) {
55 Ok(()) => println!("SIGNATURE VALID!"),
56 Err(e) => {
57 println!("SIGNATURE VERIFICATION FAILED: {:?}", e);
58 let unsigned = commit_unsigned_bytes(&commit);
59 println!("Unsigned bytes length: {} bytes", unsigned.len());
60 panic!("Signature verification failed");
61 }
62 }
63}
64
65fn commit_unsigned_bytes(commit: &jacquard_repo::commit::Commit<'_>) -> Vec<u8> {
66 #[derive(serde::Serialize)]
67 struct UnsignedCommit<'a> {
68 did: &'a str,
69 version: i64,
70 data: &'a cid::Cid,
71 rev: &'a jacquard::types::string::Tid,
72 prev: Option<&'a cid::Cid>,
73 #[serde(with = "serde_bytes")]
74 sig: &'a [u8],
75 }
76 let unsigned = UnsignedCommit {
77 did: commit.did().as_str(),
78 version: 3,
79 data: commit.data(),
80 rev: commit.rev(),
81 prev: commit.prev(),
82 sig: &[],
83 };
84 serde_ipld_dagcbor::to_vec(&unsigned).unwrap()
85}
86
87fn parse_car(
88 cursor: &mut std::io::Cursor<&[u8]>,
89) -> Result<(Vec<Cid>, HashMap<Cid, Bytes>), Box<dyn std::error::Error>> {
90 use std::io::Read;
91 fn read_varint<R: Read>(r: &mut R) -> std::io::Result<u64> {
92 let mut result = 0u64;
93 let mut shift = 0;
94 loop {
95 let mut byte = [0u8; 1];
96 r.read_exact(&mut byte)?;
97 result |= ((byte[0] & 0x7f) as u64) << shift;
98 if byte[0] & 0x80 == 0 {
99 break;
100 }
101 shift += 7;
102 }
103 Ok(result)
104 }
105 let header_len = read_varint(cursor)? as usize;
106 let mut header_bytes = vec![0u8; header_len];
107 cursor.read_exact(&mut header_bytes)?;
108 #[derive(serde::Deserialize)]
109 struct CarHeader {
110 #[allow(dead_code)]
111 version: u64,
112 roots: Vec<cid::Cid>,
113 }
114 let header: CarHeader = serde_ipld_dagcbor::from_slice(&header_bytes)?;
115 let mut blocks = HashMap::new();
116 loop {
117 let block_len = match read_varint(cursor) {
118 Ok(len) => len as usize,
119 Err(_) => break,
120 };
121 if block_len == 0 {
122 break;
123 }
124 let mut block_data = vec![0u8; block_len];
125 if cursor.read_exact(&mut block_data).is_err() {
126 break;
127 }
128 let cid_bytes = &block_data[..];
129 let (cid, cid_len) = parse_cid(cid_bytes)?;
130 let content = Bytes::copy_from_slice(&block_data[cid_len..]);
131 blocks.insert(cid, content);
132 }
133 Ok((header.roots, blocks))
134}
135fn parse_cid(bytes: &[u8]) -> Result<(Cid, usize), Box<dyn std::error::Error>> {
136 if bytes[0] == 0x01 {
137 let codec = bytes[1];
138 let _hash_type = bytes[2];
139 let hash_len = bytes[3] as usize;
140 let cid_len = 4 + hash_len;
141 let cid = Cid::new_v1(
142 codec as u64,
143 cid::multihash::Multihash::from_bytes(&bytes[2..cid_len])?,
144 );
145 Ok((cid, cid_len))
146 } else {
147 Err("Unsupported CID version".into())
148 }
149}