Server tools to backfill, tail, mirror, and verify PLC logs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

implement crypto primitives and op validation

dawn 97a6ed79 567d97e9

+464 -62
+235
Cargo.lock
··· 30 30 "clap", 31 31 "criterion", 32 32 "data-encoding", 33 + "ecdsa", 33 34 "fjall", 34 35 "futures", 35 36 "governor", 36 37 "http-body-util", 38 + "k256", 37 39 "log", 38 40 "multibase", 39 41 "native-tls", 40 42 "opentelemetry", 41 43 "opentelemetry-otlp", 42 44 "opentelemetry_sdk", 45 + "p256", 43 46 "poem", 44 47 "postgres-native-tls", 45 48 "reqwest", ··· 49 52 "rustls", 50 53 "serde", 51 54 "serde_bytes", 55 + "serde_ipld_dagcbor", 52 56 "serde_json", 53 57 "tempfile", 54 58 "thiserror 2.0.18", ··· 266 270 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 267 271 268 272 [[package]] 273 + name = "base16ct" 274 + version = "0.2.0" 275 + source = "registry+https://github.com/rust-lang/crates.io-index" 276 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 277 + 278 + [[package]] 269 279 name = "base256emoji" 270 280 version = "1.0.2" 271 281 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 282 292 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 283 293 284 294 [[package]] 295 + name = "base64ct" 296 + version = "1.8.3" 297 + source = "registry+https://github.com/rust-lang/crates.io-index" 298 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 299 + 300 + [[package]] 285 301 name = "bincode" 286 302 version = "1.3.3" 287 303 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 369 385 checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 370 386 371 387 [[package]] 388 + name = "cbor4ii" 389 + version = "0.2.14" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 392 + dependencies = [ 393 + "serde", 394 + ] 395 + 396 + [[package]] 372 397 name = "cc" 373 398 version = "1.2.56" 374 399 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 442 467 "core2", 443 468 "multibase", 444 469 "multihash", 470 + "serde", 471 + "serde_bytes", 445 472 "unsigned-varint", 446 473 ] 447 474 ··· 527 554 checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 528 555 529 556 [[package]] 557 + name = "const-oid" 558 + version = "0.9.6" 559 + source = "registry+https://github.com/rust-lang/crates.io-index" 560 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 561 + 562 + [[package]] 530 563 name = "const-str" 531 564 version = "0.4.3" 532 565 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 662 695 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 663 696 664 697 [[package]] 698 + name = "crypto-bigint" 699 + version = "0.5.5" 700 + source = "registry+https://github.com/rust-lang/crates.io-index" 701 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 702 + dependencies = [ 703 + "generic-array", 704 + "rand_core 0.6.4", 705 + "subtle", 706 + "zeroize", 707 + ] 708 + 709 + [[package]] 665 710 name = "crypto-common" 666 711 version = "0.1.7" 667 712 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 712 757 ] 713 758 714 759 [[package]] 760 + name = "der" 761 + version = "0.7.10" 762 + source = "registry+https://github.com/rust-lang/crates.io-index" 763 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 764 + dependencies = [ 765 + "const-oid", 766 + "pem-rfc7468", 767 + "zeroize", 768 + ] 769 + 770 + [[package]] 715 771 name = "der-parser" 716 772 version = "10.0.0" 717 773 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 741 797 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 742 798 dependencies = [ 743 799 "block-buffer", 800 + "const-oid", 744 801 "crypto-common", 745 802 "subtle", 746 803 ] ··· 763 820 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 764 821 765 822 [[package]] 823 + name = "ecdsa" 824 + version = "0.16.9" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 827 + dependencies = [ 828 + "der", 829 + "digest", 830 + "elliptic-curve", 831 + "rfc6979", 832 + "signature", 833 + "spki", 834 + ] 835 + 836 + [[package]] 766 837 name = "either" 767 838 version = "1.15.0" 768 839 source = "registry+https://github.com/rust-lang/crates.io-index" 769 840 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 841 + 842 + [[package]] 843 + name = "elliptic-curve" 844 + version = "0.13.8" 845 + source = "registry+https://github.com/rust-lang/crates.io-index" 846 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 847 + dependencies = [ 848 + "base16ct", 849 + "crypto-bigint", 850 + "digest", 851 + "ff", 852 + "generic-array", 853 + "group", 854 + "pem-rfc7468", 855 + "pkcs8", 856 + "rand_core 0.6.4", 857 + "sec1", 858 + "subtle", 859 + "zeroize", 860 + ] 770 861 771 862 [[package]] 772 863 name = "encoding_rs" ··· 816 907 version = "2.3.0" 817 908 source = "registry+https://github.com/rust-lang/crates.io-index" 818 909 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 910 + 911 + [[package]] 912 + name = "ff" 913 + version = "0.13.1" 914 + source = "registry+https://github.com/rust-lang/crates.io-index" 915 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 916 + dependencies = [ 917 + "rand_core 0.6.4", 918 + "subtle", 919 + ] 819 920 820 921 [[package]] 821 922 name = "find-msvc-tools" ··· 1009 1110 dependencies = [ 1010 1111 "typenum", 1011 1112 "version_check", 1113 + "zeroize", 1012 1114 ] 1013 1115 1014 1116 [[package]] ··· 1072 1174 "smallvec", 1073 1175 "spinning_top", 1074 1176 "web-time", 1177 + ] 1178 + 1179 + [[package]] 1180 + name = "group" 1181 + version = "0.13.0" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1184 + dependencies = [ 1185 + "ff", 1186 + "rand_core 0.6.4", 1187 + "subtle", 1075 1188 ] 1076 1189 1077 1190 [[package]] ··· 1461 1574 ] 1462 1575 1463 1576 [[package]] 1577 + name = "ipld-core" 1578 + version = "0.4.3" 1579 + source = "registry+https://github.com/rust-lang/crates.io-index" 1580 + checksum = "090f624976d72f0b0bb71b86d58dc16c15e069193067cb3a3a09d655246cbbda" 1581 + dependencies = [ 1582 + "cid", 1583 + "serde", 1584 + "serde_bytes", 1585 + ] 1586 + 1587 + [[package]] 1464 1588 name = "ipnet" 1465 1589 version = "2.11.0" 1466 1590 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1527 1651 ] 1528 1652 1529 1653 [[package]] 1654 + name = "k256" 1655 + version = "0.13.4" 1656 + source = "registry+https://github.com/rust-lang/crates.io-index" 1657 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1658 + dependencies = [ 1659 + "cfg-if", 1660 + "ecdsa", 1661 + "elliptic-curve", 1662 + "once_cell", 1663 + "sha2", 1664 + "signature", 1665 + ] 1666 + 1667 + [[package]] 1530 1668 name = "lazy_static" 1531 1669 version = "1.5.0" 1532 1670 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1706 1844 checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1707 1845 dependencies = [ 1708 1846 "core2", 1847 + "serde", 1709 1848 "unsigned-varint", 1710 1849 ] 1711 1850 ··· 1961 2100 ] 1962 2101 1963 2102 [[package]] 2103 + name = "p256" 2104 + version = "0.13.2" 2105 + source = "registry+https://github.com/rust-lang/crates.io-index" 2106 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2107 + dependencies = [ 2108 + "ecdsa", 2109 + "elliptic-curve", 2110 + "primeorder", 2111 + "sha2", 2112 + ] 2113 + 2114 + [[package]] 1964 2115 name = "page_size" 1965 2116 version = "0.6.0" 1966 2117 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2029 2180 ] 2030 2181 2031 2182 [[package]] 2183 + name = "pem-rfc7468" 2184 + version = "0.7.0" 2185 + source = "registry+https://github.com/rust-lang/crates.io-index" 2186 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 2187 + dependencies = [ 2188 + "base64ct", 2189 + ] 2190 + 2191 + [[package]] 2032 2192 name = "percent-encoding" 2033 2193 version = "2.3.2" 2034 2194 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2084 2244 version = "0.1.0" 2085 2245 source = "registry+https://github.com/rust-lang/crates.io-index" 2086 2246 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2247 + 2248 + [[package]] 2249 + name = "pkcs8" 2250 + version = "0.10.2" 2251 + source = "registry+https://github.com/rust-lang/crates.io-index" 2252 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 2253 + dependencies = [ 2254 + "der", 2255 + "spki", 2256 + ] 2087 2257 2088 2258 [[package]] 2089 2259 name = "pkg-config" ··· 2258 2428 ] 2259 2429 2260 2430 [[package]] 2431 + name = "primeorder" 2432 + version = "0.13.6" 2433 + source = "registry+https://github.com/rust-lang/crates.io-index" 2434 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2435 + dependencies = [ 2436 + "elliptic-curve", 2437 + ] 2438 + 2439 + [[package]] 2261 2440 name = "proc-macro-crate" 2262 2441 version = "3.4.0" 2263 2442 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2635 2814 ] 2636 2815 2637 2816 [[package]] 2817 + name = "rfc6979" 2818 + version = "0.4.0" 2819 + source = "registry+https://github.com/rust-lang/crates.io-index" 2820 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 2821 + dependencies = [ 2822 + "hmac", 2823 + "subtle", 2824 + ] 2825 + 2826 + [[package]] 2638 2827 name = "rfc7239" 2639 2828 version = "0.1.3" 2640 2829 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2800 2989 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2801 2990 2802 2991 [[package]] 2992 + name = "sec1" 2993 + version = "0.7.3" 2994 + source = "registry+https://github.com/rust-lang/crates.io-index" 2995 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2996 + dependencies = [ 2997 + "base16ct", 2998 + "der", 2999 + "generic-array", 3000 + "pkcs8", 3001 + "subtle", 3002 + "zeroize", 3003 + ] 3004 + 3005 + [[package]] 2803 3006 name = "security-framework" 2804 3007 version = "3.7.0" 2805 3008 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2875 3078 ] 2876 3079 2877 3080 [[package]] 3081 + name = "serde_ipld_dagcbor" 3082 + version = "0.6.4" 3083 + source = "registry+https://github.com/rust-lang/crates.io-index" 3084 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 3085 + dependencies = [ 3086 + "cbor4ii", 3087 + "ipld-core", 3088 + "scopeguard", 3089 + "serde", 3090 + ] 3091 + 3092 + [[package]] 2878 3093 name = "serde_json" 2879 3094 version = "1.0.149" 2880 3095 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2958 3173 ] 2959 3174 2960 3175 [[package]] 3176 + name = "signature" 3177 + version = "2.2.0" 3178 + source = "registry+https://github.com/rust-lang/crates.io-index" 3179 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3180 + dependencies = [ 3181 + "digest", 3182 + "rand_core 0.6.4", 3183 + ] 3184 + 3185 + [[package]] 2961 3186 name = "simd-adler32" 2962 3187 version = "0.3.8" 2963 3188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3007 3232 checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" 3008 3233 dependencies = [ 3009 3234 "lock_api", 3235 + ] 3236 + 3237 + [[package]] 3238 + name = "spki" 3239 + version = "0.7.3" 3240 + source = "registry+https://github.com/rust-lang/crates.io-index" 3241 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 3242 + dependencies = [ 3243 + "base64ct", 3244 + "der", 3010 3245 ] 3011 3246 3012 3247 [[package]]
+4
Cargo.toml
··· 49 49 bincode = "1.3.3" 50 50 serde_bytes = "0.11.19" 51 51 multibase = "0.9.2" 52 + ecdsa = "0.16.9" 53 + p256 = "0.13.2" 54 + k256 = "0.13.4" 55 + serde_ipld_dagcbor = "0.6.4" 52 56
+219
src/crypto.rs
··· 1 + use data_encoding::BASE64URL_NOPAD; 2 + use serde::{Deserialize, Serialize}; 3 + use std::fmt; 4 + 5 + /// base64url-encoded ECDSA signature → raw bytes 6 + #[derive(Debug, Clone, Serialize, Deserialize)] 7 + pub struct Signature(#[serde(with = "serde_bytes")] pub Vec<u8>); 8 + 9 + impl 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 + 18 + impl 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)] 26 + pub struct DidKey(#[serde(with = "serde_bytes")] pub Vec<u8>); 27 + 28 + impl 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 + 39 + impl 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 + 49 + const P256_PREFIX: [u8; 2] = [0x80, 0x24]; 50 + const 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 57 + pub 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 + 102 + pub 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. 112 + pub 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)] 141 + mod 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 + "no keys to verify against for {}", 203 + path 204 + ); 205 + 206 + let results = assure_valid_sig(&valid_keys, &sig, &data) 207 + .expect("that we used the function correctly"); 208 + for err in results.errors { 209 + println!("{path}/{cid}: {err}"); 210 + } 211 + if !results.valid { 212 + panic!("signature verification failed in {path}/{cid}"); 213 + } 214 + 215 + ops_by_cid.insert(cid, data); 216 + } 217 + } 218 + } 219 + }
+1
src/lib.rs
··· 5 5 mod backfill; 6 6 mod cached_value; 7 7 mod client; 8 + mod crypto; 8 9 pub mod doc; 9 10 mod mirror; 10 11 mod plc_fjall;
+5 -62
src/plc_fjall.rs
··· 1 - use crate::{BundleSource, Week}; 2 - use crate::{Dt, ExportPage, Op as CommonOp, PageBoundaryState}; 1 + use crate::{ 2 + BundleSource, Dt, ExportPage, Op as CommonOp, PageBoundaryState, Week, 3 + crypto::{DidKey, Signature}, 4 + }; 3 5 use anyhow::Context; 4 - use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD}; 6 + use data_encoding::BASE32_NOPAD; 5 7 use fjall::{ 6 8 Database, Keyspace, KeyspaceCreateOptions, OwnedWriteBatch, PersistMode, 7 9 config::BlockSizePolicy, ··· 87 89 ); 88 90 Dt::from_timestamp_micros(micros as i64) 89 91 .ok_or_else(|| anyhow::anyhow!("invalid timestamp {micros}")) 90 - } 91 - 92 - /// base64url-encoded ECDSA signature → raw bytes 93 - #[derive(Debug, Clone, Serialize, Deserialize)] 94 - struct Signature(#[serde(with = "serde_bytes")] Vec<u8>); 95 - 96 - impl Signature { 97 - fn from_base64url(s: &str) -> anyhow::Result<Self> { 98 - BASE64URL_NOPAD 99 - .decode(s.as_bytes()) 100 - .map(Self) 101 - .map_err(|e| anyhow::anyhow!("invalid base64url sig {s}: {e}")) 102 - } 103 - } 104 - 105 - impl fmt::Display for Signature { 106 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 - f.write_str(&BASE64URL_NOPAD.encode(&self.0)) 108 - } 109 - } 110 - 111 - /// did:key:z... → raw multicodec public key bytes 112 - #[derive(Debug, Clone, Serialize, Deserialize)] 113 - struct DidKey(#[serde(with = "serde_bytes")] Vec<u8>); 114 - 115 - impl DidKey { 116 - fn from_did_key(s: &str) -> anyhow::Result<Self> { 117 - let multibase_str = s 118 - .strip_prefix("did:key:") 119 - .ok_or_else(|| anyhow::anyhow!("missing did:key: prefix in {s}"))?; 120 - let (_base, bytes) = multibase::decode(multibase_str) 121 - .map_err(|e| anyhow::anyhow!("invalid multibase in did:key {s}: {e}"))?; 122 - Ok(Self(bytes)) 123 - } 124 - } 125 - 126 - impl fmt::Display for DidKey { 127 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 - write!( 129 - f, 130 - "did:key:{}", 131 - multibase::encode(multibase::Base::Base58Btc, &self.0) 132 - ) 133 - } 134 92 } 135 93 136 94 /// CID string → binary CID bytes ··· 1213 1171 #[cfg(test)] 1214 1172 mod tests { 1215 1173 use super::*; 1216 - 1217 - #[test] 1218 - fn signature_roundtrip() { 1219 - let original = "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA"; 1220 - let sig = Signature::from_base64url(original).unwrap(); 1221 - assert_eq!(sig.0.len(), 64); 1222 - assert_eq!(sig.to_string(), original); 1223 - } 1224 - 1225 - #[test] 1226 - fn did_key_roundtrip() { 1227 - let original = "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg"; 1228 - let key = DidKey::from_did_key(original).unwrap(); 1229 - assert_eq!(key.to_string(), original); 1230 - } 1231 1174 1232 1175 #[test] 1233 1176 fn plc_cid_roundtrip() {