Server tools to backfill, tail, mirror, and verify PLC logs
at main 218 lines 7.7 kB view raw
1use data_encoding::BASE64URL_NOPAD; 2use serde::{Deserialize, Serialize}; 3use std::fmt; 4 5/// base64url-encoded ECDSA signature → raw bytes 6#[derive(Debug, Clone, Serialize, Deserialize, bitcode::Encode, bitcode::Decode)] 7pub struct Signature(#[serde(with = "serde_bytes")] pub Vec<u8>); 8 9impl Signature { 10 pub fn from_base64url(s: &str) -> anyhow::Result<Self> { 11 BASE64URL_NOPAD 12 .decode(s.as_bytes()) 13 .map(Self) 14 .map_err(|e| anyhow::anyhow!("invalid base64url sig {s}: {e}")) 15 } 16} 17 18impl fmt::Display for Signature { 19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 f.write_str(&BASE64URL_NOPAD.encode(&self.0)) 21 } 22} 23 24/// did:key:z... → raw multicodec public key bytes 25#[derive(Debug, Clone, Serialize, Deserialize, bitcode::Encode, bitcode::Decode)] 26pub struct DidKey(#[serde(with = "serde_bytes")] pub Vec<u8>); 27 28impl DidKey { 29 pub fn from_did_key(s: &str) -> anyhow::Result<Self> { 30 let multibase_str = s 31 .strip_prefix("did:key:") 32 .ok_or_else(|| anyhow::anyhow!("missing did:key: prefix in {s}"))?; 33 let (_base, bytes) = multibase::decode(multibase_str) 34 .map_err(|e| anyhow::anyhow!("invalid multibase in did:key {s}: {e}"))?; 35 Ok(Self(bytes)) 36 } 37} 38 39impl fmt::Display for DidKey { 40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 write!( 42 f, 43 "did:key:{}", 44 multibase::encode(multibase::Base::Base58Btc, &self.0) 45 ) 46 } 47} 48 49const P256_PREFIX: [u8; 2] = [0x80, 0x24]; 50const K256_PREFIX: [u8; 2] = [0xe7, 0x01]; 51 52/// verifies a plc op signature 53/// 54/// - `key` : did:key:z... public key 55/// - `data`: dag-cbor encoded op without the `sig` field (sha256 is applied internally) 56/// - `sig` : signature bytes decoded from the base64url `sig` field of the op 57pub fn verify_plc_sig(key: &DidKey, data: &[u8], sig: &Signature) -> anyhow::Result<()> { 58 use ecdsa::signature::Verifier as _; 59 60 let prefix: [u8; 2] = key 61 .0 62 .get(..2) 63 .ok_or_else(|| anyhow::anyhow!("key bytes too short: {key}"))? 64 .try_into() 65 .map_err(|_| anyhow::anyhow!("key bytes too short: {key}"))?; 66 let pubkey = key 67 .0 68 .get(2..) 69 .ok_or_else(|| anyhow::anyhow!("key bytes too short: {key}"))?; 70 71 match prefix { 72 P256_PREFIX => { 73 use p256::ecdsa::{Signature, VerifyingKey}; 74 75 let key = VerifyingKey::from_sec1_bytes(pubkey) 76 .map_err(|e| anyhow::anyhow!("bad p256 key {pubkey:?}: {e}"))?; 77 let sig = Signature::from_slice(&sig.0) 78 .map_err(|e| anyhow::anyhow!("bad p256 sig {sig}: {e}"))?; 79 if sig.normalize_s().is_some() { 80 anyhow::bail!("high-S signature is not allowed for plc"); 81 } 82 key.verify(data, &sig) 83 .map_err(|e| anyhow::anyhow!("invalid p256 signature {sig}: {e}")) 84 } 85 K256_PREFIX => { 86 use k256::ecdsa::{Signature, VerifyingKey}; 87 88 let key = VerifyingKey::from_sec1_bytes(pubkey) 89 .map_err(|e| anyhow::anyhow!("bad k256 key {pubkey:?}: {e}"))?; 90 let sig = Signature::from_slice(&sig.0) 91 .map_err(|e| anyhow::anyhow!("bad k256 sig {sig}: {e}"))?; 92 if sig.normalize_s().is_some() { 93 anyhow::bail!("high-S signature is not allowed for plc"); 94 } 95 key.verify(data, &sig) 96 .map_err(|e| anyhow::anyhow!("invalid k256 signature {sig}: {e}")) 97 } 98 _ => anyhow::bail!("unsupported key prefix: {:02x?}", prefix), 99 } 100} 101 102pub struct AssuranceResults { 103 pub valid: bool, 104 pub errors: Vec<anyhow::Error>, 105} 106 107/// assures that an op has a valid signature 108/// 109/// - `keys`: the rotation keys from the previous operation (or it's own keys if genesis op) 110/// - `sig` : the signature to check. 111/// - `data`: the operation to check, without the sig field. 112pub fn assure_valid_sig<'key>( 113 keys: impl IntoIterator<Item = &'key DidKey>, 114 sig: &Signature, 115 data: &serde_json::Value, 116) -> anyhow::Result<AssuranceResults> { 117 let serde_json::Value::Object(data) = data else { 118 anyhow::bail!("invalid op, not an object"); 119 }; 120 if data.contains_key("sig") { 121 anyhow::bail!("data should not include the sig"); 122 } 123 let data = serde_ipld_dagcbor::to_vec(&data)?; 124 let mut results = AssuranceResults { 125 valid: false, 126 errors: Vec::new(), 127 }; 128 for key in keys { 129 match verify_plc_sig(key, &data, sig) { 130 Ok(_) => { 131 results.valid = true; 132 break; 133 } 134 Err(e) => results.errors.push(e), 135 } 136 } 137 Ok(results) 138} 139 140#[cfg(test)] 141mod tests { 142 use super::*; 143 use std::collections::HashMap; 144 145 #[test] 146 fn signature_roundtrip() { 147 let original = "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA"; 148 let sig = Signature::from_base64url(original).unwrap(); 149 assert_eq!(sig.0.len(), 64); 150 assert_eq!(sig.to_string(), original); 151 } 152 153 #[test] 154 fn did_key_roundtrip() { 155 let original = "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg"; 156 let key = DidKey::from_did_key(original).unwrap(); 157 assert_eq!(key.to_string(), original); 158 } 159 160 #[test] 161 fn test_fixture_signatures() { 162 let fixtures = [ 163 "tests/fixtures/log_bskyapp.json", 164 "tests/fixtures/log_legacy_dholms.json", 165 "tests/fixtures/log_nullification.json", 166 "tests/fixtures/log_tombstone.json", 167 ]; 168 169 for path in fixtures { 170 let data = std::fs::read_to_string(path).unwrap(); 171 let entries: Vec<serde_json::Value> = serde_json::from_str(&data).unwrap(); 172 173 let mut ops_by_cid: HashMap<String, serde_json::Value> = HashMap::new(); 174 175 for entry in entries { 176 let mut data = entry["operation"].clone(); 177 let cid = entry["cid"].as_str().unwrap().to_string(); 178 179 let sig_str = data["sig"].as_str().unwrap(); 180 let sig = Signature::from_base64url(sig_str).unwrap(); 181 182 data.as_object_mut().unwrap().remove("sig"); 183 184 let prev_cid = data["prev"].as_str().unwrap_or(""); 185 let op = ops_by_cid.get(prev_cid).unwrap_or(&data); 186 187 let mut valid_keys = Vec::new(); 188 if let Some(arr) = op["rotationKeys"].as_array() { 189 for k in arr { 190 valid_keys.push(DidKey::from_did_key(k.as_str().unwrap()).unwrap()); 191 } 192 } 193 if let Some(rk) = op["recoveryKey"].as_str() { 194 valid_keys.push(DidKey::from_did_key(rk).unwrap()); 195 } 196 if let Some(sk) = op["signingKey"].as_str() { 197 valid_keys.push(DidKey::from_did_key(sk).unwrap()); 198 } 199 200 assert!( 201 !valid_keys.is_empty(), 202 "{path}/{cid}: no keys to verify against" 203 ); 204 205 let results = assure_valid_sig(&valid_keys, &sig, &data) 206 .expect("that we used the function correctly"); 207 for err in results.errors { 208 println!("{path}/{cid}: {err}"); 209 } 210 if !results.valid { 211 panic!("signature verification failed in {path}/{cid}"); 212 } 213 214 ops_by_cid.insert(cid, data); 215 } 216 } 217 } 218}