forked from
microcosm.blue/Allegedly
Server tools to backfill, tail, mirror, and verify PLC logs
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}