A library for ATProtocol identities.

Add did-method-key support

* Add comprehensive CLI binary documentation
* Enhanced atproto-identity-resolve.rs with detailed usage examples and configuration options
* Added complete documentation for atproto-identity-sign.rs covering IPLD DAG-CBOR signing workflow
* Added comprehensive documentation for atproto-identity-validate.rs with signature verification examples
* Included practical command-line examples, argument descriptions, and error handling details
* Documented supported cryptographic key types (P-256, K-256) and multibase encoding
* Bumping version to 0.2.0

+16 -3
CLAUDE.md
··· 20 ## Architecture 21 22 A comprehensive Rust library with: 23 - - Modular architecture with 7 core modules (resolve, plc, web, model, validation, config, errors) 24 - Complete CLI tool for identity resolution (`atproto-identity-resolve`) 25 - Rust edition 2021 with modern async/await patterns 26 - Comprehensive error handling with structured error types ··· 37 38 * error-atproto-identity-resolve-1 Multiple DIDs resolved for method 39 * error-atproto-identity-plc-1 HTTP request failed: https://google.com/ Not Found 40 - * error-did-web-1 Invalid DID format: missing 'did:web:' prefix 41 42 Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. 43 ··· 51 } 52 ``` 53 54 - ### Logging 55 56 Use tracing for structured logging. 57 ··· 67 - **`src/validation.rs`**: Input validation for handles and DIDs 68 - **`src/config.rs`**: Configuration management and environment variable handling 69 - **`src/errors.rs`**: Structured error types following project conventions 70 - **`src/bin/atproto-identity-resolve.rs`**: CLI tool for identity resolution
··· 20 ## Architecture 21 22 A comprehensive Rust library with: 23 + - Modular architecture with 8 core modules (resolve, plc, web, model, validation, config, errors, key) 24 - Complete CLI tool for identity resolution (`atproto-identity-resolve`) 25 - Rust edition 2021 with modern async/await patterns 26 - Comprehensive error handling with structured error types ··· 37 38 * error-atproto-identity-resolve-1 Multiple DIDs resolved for method 39 * error-atproto-identity-plc-1 HTTP request failed: https://google.com/ Not Found 40 + * error-atproto-identity-key-1 Error decoding key: invalid 41 42 Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. 43 ··· 51 } 52 ``` 53 54 + ## Result 55 + 56 + Functions that return a `Result` type should use `anyhow::Result` where second Error component of is one of the error types defined in `src/errors.rs`. 57 + 58 + ## Logging 59 60 Use tracing for structured logging. 61 ··· 71 - **`src/validation.rs`**: Input validation for handles and DIDs 72 - **`src/config.rs`**: Configuration management and environment variable handling 73 - **`src/errors.rs`**: Structured error types following project conventions 74 + - **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 75 - **`src/bin/atproto-identity-resolve.rs`**: CLI tool for identity resolution 76 + 77 + ## Documentation 78 + 79 + All public and exported types, methods, and variables must be documented. 80 + 81 + All source files must have high level module documentation. 82 + 83 + Documentation must be brief and specific.
+412 -3
Cargo.lock
··· 51 52 [[package]] 53 name = "atproto-identity" 54 - version = "0.1.1" 55 dependencies = [ 56 "anyhow", 57 "futures-util", 58 "hickory-resolver", 59 "reqwest", 60 "serde", 61 "serde_json", 62 "thiserror 2.0.12", 63 "tokio", ··· 86 ] 87 88 [[package]] 89 name = "base64" 90 version = "0.22.1" 91 source = "registry+https://github.com/rust-lang/crates.io-index" 92 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 93 94 [[package]] 95 name = "bitflags" 96 version = "2.9.1" 97 source = "registry+https://github.com/rust-lang/crates.io-index" 98 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 99 100 [[package]] 101 name = "bumpalo" 102 version = "3.17.0" 103 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 110 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 111 112 [[package]] 113 name = "cc" 114 version = "1.2.24" 115 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 131 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 132 133 [[package]] 134 name = "core-foundation" 135 version = "0.9.4" 136 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 147 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 148 149 [[package]] 150 name = "critical-section" 151 version = "1.2.0" 152 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 177 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 178 179 [[package]] 180 name = "data-encoding" 181 version = "2.9.0" 182 source = "registry+https://github.com/rust-lang/crates.io-index" 183 checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 184 185 [[package]] 186 name = "displaydoc" 187 version = "0.2.5" 188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 194 ] 195 196 [[package]] 197 name = "encoding_rs" 198 version = "0.8.35" 199 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 237 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 238 239 [[package]] 240 name = "fnv" 241 version = "1.0.7" 242 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 339 ] 340 341 [[package]] 342 name = "getrandom" 343 version = "0.2.16" 344 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 372 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 373 374 [[package]] 375 name = "h2" 376 version = "0.4.10" 377 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 446 "thiserror 2.0.12", 447 "tokio", 448 "tracing", 449 ] 450 451 [[package]] ··· 697 ] 698 699 [[package]] 700 name = "ipnet" 701 version = "2.11.0" 702 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 729 ] 730 731 [[package]] 732 name = "lazy_static" 733 version = "1.5.0" 734 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 848 ] 849 850 [[package]] 851 name = "native-tls" 852 version = "0.2.14" 853 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 944 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 945 946 [[package]] 947 name = "parking_lot" 948 version = "0.12.4" 949 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 967 ] 968 969 [[package]] 970 name = "percent-encoding" 971 version = "2.3.1" 972 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 985 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 986 987 [[package]] 988 name = "pkg-config" 989 version = "0.3.32" 990 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1012 checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1013 dependencies = [ 1014 "zerocopy", 1015 ] 1016 1017 [[package]] ··· 1100 checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1101 dependencies = [ 1102 "rand_chacha", 1103 - "rand_core", 1104 ] 1105 1106 [[package]] ··· 1110 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1111 dependencies = [ 1112 "ppv-lite86", 1113 - "rand_core", 1114 ] 1115 1116 [[package]] ··· 1228 checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 1229 1230 [[package]] 1231 name = "ring" 1232 version = "0.17.14" 1233 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1344 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1345 1346 [[package]] 1347 name = "security-framework" 1348 version = "2.11.1" 1349 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1382 ] 1383 1384 [[package]] 1385 name = "serde_derive" 1386 version = "1.0.219" 1387 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1393 ] 1394 1395 [[package]] 1396 name = "serde_json" 1397 version = "1.0.140" 1398 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1417 ] 1418 1419 [[package]] 1420 name = "sharded-slab" 1421 version = "0.1.7" 1422 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1432 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1433 1434 [[package]] 1435 name = "slab" 1436 version = "0.4.9" 1437 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1454 dependencies = [ 1455 "libc", 1456 "windows-sys 0.52.0", 1457 ] 1458 1459 [[package]] ··· 1787 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1788 1789 [[package]] 1790 name = "unicode-ident" 1791 version = "1.0.18" 1792 source = "registry+https://github.com/rust-lang/crates.io-index" 1793 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1794 1795 [[package]] 1796 name = "untrusted" ··· 1837 version = "0.2.15" 1838 source = "registry+https://github.com/rust-lang/crates.io-index" 1839 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1840 1841 [[package]] 1842 name = "want"
··· 51 52 [[package]] 53 name = "atproto-identity" 54 + version = "0.2.0" 55 dependencies = [ 56 "anyhow", 57 + "ecdsa", 58 "futures-util", 59 "hickory-resolver", 60 + "k256", 61 + "multibase", 62 + "p256", 63 "reqwest", 64 "serde", 65 + "serde_ipld_dagcbor", 66 "serde_json", 67 "thiserror 2.0.12", 68 "tokio", ··· 91 ] 92 93 [[package]] 94 + name = "base-x" 95 + version = "0.2.11" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 98 + 99 + [[package]] 100 + name = "base16ct" 101 + version = "0.2.0" 102 + source = "registry+https://github.com/rust-lang/crates.io-index" 103 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 104 + 105 + [[package]] 106 name = "base64" 107 version = "0.22.1" 108 source = "registry+https://github.com/rust-lang/crates.io-index" 109 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 110 111 [[package]] 112 + name = "base64ct" 113 + version = "1.7.3" 114 + source = "registry+https://github.com/rust-lang/crates.io-index" 115 + checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" 116 + 117 + [[package]] 118 name = "bitflags" 119 version = "2.9.1" 120 source = "registry+https://github.com/rust-lang/crates.io-index" 121 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 122 123 [[package]] 124 + name = "block-buffer" 125 + version = "0.10.4" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 128 + dependencies = [ 129 + "generic-array", 130 + ] 131 + 132 + [[package]] 133 name = "bumpalo" 134 version = "3.17.0" 135 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 142 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 143 144 [[package]] 145 + name = "cbor4ii" 146 + version = "0.2.14" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 149 + dependencies = [ 150 + "serde", 151 + ] 152 + 153 + [[package]] 154 name = "cc" 155 version = "1.2.24" 156 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 172 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 173 174 [[package]] 175 + name = "cid" 176 + version = "0.11.1" 177 + source = "registry+https://github.com/rust-lang/crates.io-index" 178 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 179 + dependencies = [ 180 + "core2", 181 + "multibase", 182 + "multihash", 183 + "serde", 184 + "serde_bytes", 185 + "unsigned-varint", 186 + ] 187 + 188 + [[package]] 189 + name = "const-oid" 190 + version = "0.9.6" 191 + source = "registry+https://github.com/rust-lang/crates.io-index" 192 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 193 + 194 + [[package]] 195 name = "core-foundation" 196 version = "0.9.4" 197 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 208 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 209 210 [[package]] 211 + name = "core2" 212 + version = "0.4.0" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 215 + dependencies = [ 216 + "memchr", 217 + ] 218 + 219 + [[package]] 220 + name = "cpufeatures" 221 + version = "0.2.17" 222 + source = "registry+https://github.com/rust-lang/crates.io-index" 223 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 224 + dependencies = [ 225 + "libc", 226 + ] 227 + 228 + [[package]] 229 name = "critical-section" 230 version = "1.2.0" 231 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 256 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 257 258 [[package]] 259 + name = "crypto-bigint" 260 + version = "0.5.5" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 263 + dependencies = [ 264 + "generic-array", 265 + "rand_core 0.6.4", 266 + "subtle", 267 + "zeroize", 268 + ] 269 + 270 + [[package]] 271 + name = "crypto-common" 272 + version = "0.1.6" 273 + source = "registry+https://github.com/rust-lang/crates.io-index" 274 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 275 + dependencies = [ 276 + "generic-array", 277 + "typenum", 278 + ] 279 + 280 + [[package]] 281 name = "data-encoding" 282 version = "2.9.0" 283 source = "registry+https://github.com/rust-lang/crates.io-index" 284 checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 285 286 [[package]] 287 + name = "data-encoding-macro" 288 + version = "0.1.18" 289 + source = "registry+https://github.com/rust-lang/crates.io-index" 290 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 291 + dependencies = [ 292 + "data-encoding", 293 + "data-encoding-macro-internal", 294 + ] 295 + 296 + [[package]] 297 + name = "data-encoding-macro-internal" 298 + version = "0.1.16" 299 + source = "registry+https://github.com/rust-lang/crates.io-index" 300 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 301 + dependencies = [ 302 + "data-encoding", 303 + "syn", 304 + ] 305 + 306 + [[package]] 307 + name = "der" 308 + version = "0.7.10" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 311 + dependencies = [ 312 + "const-oid", 313 + "pem-rfc7468", 314 + "zeroize", 315 + ] 316 + 317 + [[package]] 318 + name = "digest" 319 + version = "0.10.7" 320 + source = "registry+https://github.com/rust-lang/crates.io-index" 321 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 322 + dependencies = [ 323 + "block-buffer", 324 + "const-oid", 325 + "crypto-common", 326 + "subtle", 327 + ] 328 + 329 + [[package]] 330 name = "displaydoc" 331 version = "0.2.5" 332 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 338 ] 339 340 [[package]] 341 + name = "ecdsa" 342 + version = "0.16.9" 343 + source = "registry+https://github.com/rust-lang/crates.io-index" 344 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 345 + dependencies = [ 346 + "der", 347 + "digest", 348 + "elliptic-curve", 349 + "rfc6979", 350 + "signature", 351 + "spki", 352 + ] 353 + 354 + [[package]] 355 + name = "elliptic-curve" 356 + version = "0.13.8" 357 + source = "registry+https://github.com/rust-lang/crates.io-index" 358 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 359 + dependencies = [ 360 + "base16ct", 361 + "crypto-bigint", 362 + "digest", 363 + "ff", 364 + "generic-array", 365 + "group", 366 + "pem-rfc7468", 367 + "pkcs8", 368 + "rand_core 0.6.4", 369 + "sec1", 370 + "subtle", 371 + "zeroize", 372 + ] 373 + 374 + [[package]] 375 name = "encoding_rs" 376 version = "0.8.35" 377 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 415 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 416 417 [[package]] 418 + name = "ff" 419 + version = "0.13.1" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 422 + dependencies = [ 423 + "rand_core 0.6.4", 424 + "subtle", 425 + ] 426 + 427 + [[package]] 428 name = "fnv" 429 version = "1.0.7" 430 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 527 ] 528 529 [[package]] 530 + name = "generic-array" 531 + version = "0.14.7" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 534 + dependencies = [ 535 + "typenum", 536 + "version_check", 537 + "zeroize", 538 + ] 539 + 540 + [[package]] 541 name = "getrandom" 542 version = "0.2.16" 543 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 571 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 572 573 [[package]] 574 + name = "group" 575 + version = "0.13.0" 576 + source = "registry+https://github.com/rust-lang/crates.io-index" 577 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 578 + dependencies = [ 579 + "ff", 580 + "rand_core 0.6.4", 581 + "subtle", 582 + ] 583 + 584 + [[package]] 585 name = "h2" 586 version = "0.4.10" 587 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 656 "thiserror 2.0.12", 657 "tokio", 658 "tracing", 659 + ] 660 + 661 + [[package]] 662 + name = "hmac" 663 + version = "0.12.1" 664 + source = "registry+https://github.com/rust-lang/crates.io-index" 665 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 666 + dependencies = [ 667 + "digest", 668 ] 669 670 [[package]] ··· 916 ] 917 918 [[package]] 919 + name = "ipld-core" 920 + version = "0.4.2" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 923 + dependencies = [ 924 + "cid", 925 + "serde", 926 + "serde_bytes", 927 + ] 928 + 929 + [[package]] 930 name = "ipnet" 931 version = "2.11.0" 932 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 959 ] 960 961 [[package]] 962 + name = "k256" 963 + version = "0.13.4" 964 + source = "registry+https://github.com/rust-lang/crates.io-index" 965 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 966 + dependencies = [ 967 + "cfg-if", 968 + "ecdsa", 969 + "elliptic-curve", 970 + "once_cell", 971 + "sha2", 972 + "signature", 973 + ] 974 + 975 + [[package]] 976 name = "lazy_static" 977 version = "1.5.0" 978 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1092 ] 1093 1094 [[package]] 1095 + name = "multibase" 1096 + version = "0.9.1" 1097 + source = "registry+https://github.com/rust-lang/crates.io-index" 1098 + checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 1099 + dependencies = [ 1100 + "base-x", 1101 + "data-encoding", 1102 + "data-encoding-macro", 1103 + ] 1104 + 1105 + [[package]] 1106 + name = "multihash" 1107 + version = "0.19.3" 1108 + source = "registry+https://github.com/rust-lang/crates.io-index" 1109 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1110 + dependencies = [ 1111 + "core2", 1112 + "serde", 1113 + "unsigned-varint", 1114 + ] 1115 + 1116 + [[package]] 1117 name = "native-tls" 1118 version = "0.2.14" 1119 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1210 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1211 1212 [[package]] 1213 + name = "p256" 1214 + version = "0.13.2" 1215 + source = "registry+https://github.com/rust-lang/crates.io-index" 1216 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 1217 + dependencies = [ 1218 + "ecdsa", 1219 + "elliptic-curve", 1220 + "primeorder", 1221 + "sha2", 1222 + ] 1223 + 1224 + [[package]] 1225 name = "parking_lot" 1226 version = "0.12.4" 1227 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1245 ] 1246 1247 [[package]] 1248 + name = "pem-rfc7468" 1249 + version = "0.7.0" 1250 + source = "registry+https://github.com/rust-lang/crates.io-index" 1251 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1252 + dependencies = [ 1253 + "base64ct", 1254 + ] 1255 + 1256 + [[package]] 1257 name = "percent-encoding" 1258 version = "2.3.1" 1259 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1272 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1273 1274 [[package]] 1275 + name = "pkcs8" 1276 + version = "0.10.2" 1277 + source = "registry+https://github.com/rust-lang/crates.io-index" 1278 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1279 + dependencies = [ 1280 + "der", 1281 + "spki", 1282 + ] 1283 + 1284 + [[package]] 1285 name = "pkg-config" 1286 version = "0.3.32" 1287 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1309 checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1310 dependencies = [ 1311 "zerocopy", 1312 + ] 1313 + 1314 + [[package]] 1315 + name = "primeorder" 1316 + version = "0.13.6" 1317 + source = "registry+https://github.com/rust-lang/crates.io-index" 1318 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 1319 + dependencies = [ 1320 + "elliptic-curve", 1321 ] 1322 1323 [[package]] ··· 1406 checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1407 dependencies = [ 1408 "rand_chacha", 1409 + "rand_core 0.9.3", 1410 ] 1411 1412 [[package]] ··· 1416 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1417 dependencies = [ 1418 "ppv-lite86", 1419 + "rand_core 0.9.3", 1420 + ] 1421 + 1422 + [[package]] 1423 + name = "rand_core" 1424 + version = "0.6.4" 1425 + source = "registry+https://github.com/rust-lang/crates.io-index" 1426 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1427 + dependencies = [ 1428 + "getrandom 0.2.16", 1429 ] 1430 1431 [[package]] ··· 1543 checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 1544 1545 [[package]] 1546 + name = "rfc6979" 1547 + version = "0.4.0" 1548 + source = "registry+https://github.com/rust-lang/crates.io-index" 1549 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 1550 + dependencies = [ 1551 + "hmac", 1552 + "subtle", 1553 + ] 1554 + 1555 + [[package]] 1556 name = "ring" 1557 version = "0.17.14" 1558 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1669 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1670 1671 [[package]] 1672 + name = "sec1" 1673 + version = "0.7.3" 1674 + source = "registry+https://github.com/rust-lang/crates.io-index" 1675 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 1676 + dependencies = [ 1677 + "base16ct", 1678 + "der", 1679 + "generic-array", 1680 + "pkcs8", 1681 + "subtle", 1682 + "zeroize", 1683 + ] 1684 + 1685 + [[package]] 1686 name = "security-framework" 1687 version = "2.11.1" 1688 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1721 ] 1722 1723 [[package]] 1724 + name = "serde_bytes" 1725 + version = "0.11.17" 1726 + source = "registry+https://github.com/rust-lang/crates.io-index" 1727 + checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 1728 + dependencies = [ 1729 + "serde", 1730 + ] 1731 + 1732 + [[package]] 1733 name = "serde_derive" 1734 version = "1.0.219" 1735 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1741 ] 1742 1743 [[package]] 1744 + name = "serde_ipld_dagcbor" 1745 + version = "0.6.3" 1746 + source = "registry+https://github.com/rust-lang/crates.io-index" 1747 + checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 1748 + dependencies = [ 1749 + "cbor4ii", 1750 + "ipld-core", 1751 + "scopeguard", 1752 + "serde", 1753 + ] 1754 + 1755 + [[package]] 1756 name = "serde_json" 1757 version = "1.0.140" 1758 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1777 ] 1778 1779 [[package]] 1780 + name = "sha2" 1781 + version = "0.10.9" 1782 + source = "registry+https://github.com/rust-lang/crates.io-index" 1783 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1784 + dependencies = [ 1785 + "cfg-if", 1786 + "cpufeatures", 1787 + "digest", 1788 + ] 1789 + 1790 + [[package]] 1791 name = "sharded-slab" 1792 version = "0.1.7" 1793 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1803 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1804 1805 [[package]] 1806 + name = "signature" 1807 + version = "2.2.0" 1808 + source = "registry+https://github.com/rust-lang/crates.io-index" 1809 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1810 + dependencies = [ 1811 + "digest", 1812 + "rand_core 0.6.4", 1813 + ] 1814 + 1815 + [[package]] 1816 name = "slab" 1817 version = "0.4.9" 1818 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1835 dependencies = [ 1836 "libc", 1837 "windows-sys 0.52.0", 1838 + ] 1839 + 1840 + [[package]] 1841 + name = "spki" 1842 + version = "0.7.3" 1843 + source = "registry+https://github.com/rust-lang/crates.io-index" 1844 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1845 + dependencies = [ 1846 + "base64ct", 1847 + "der", 1848 ] 1849 1850 [[package]] ··· 2178 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2179 2180 [[package]] 2181 + name = "typenum" 2182 + version = "1.18.0" 2183 + source = "registry+https://github.com/rust-lang/crates.io-index" 2184 + checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2185 + 2186 + [[package]] 2187 name = "unicode-ident" 2188 version = "1.0.18" 2189 source = "registry+https://github.com/rust-lang/crates.io-index" 2190 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2191 + 2192 + [[package]] 2193 + name = "unsigned-varint" 2194 + version = "0.8.0" 2195 + source = "registry+https://github.com/rust-lang/crates.io-index" 2196 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2197 2198 [[package]] 2199 name = "untrusted" ··· 2240 version = "0.2.15" 2241 source = "registry+https://github.com/rust-lang/crates.io-index" 2242 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2243 + 2244 + [[package]] 2245 + name = "version_check" 2246 + version = "0.9.5" 2247 + source = "registry+https://github.com/rust-lang/crates.io-index" 2248 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2249 2250 [[package]] 2251 name = "want"
+19 -1
Cargo.toml
··· 1 [package] 2 name = "atproto-identity" 3 - version = "0.1.1" 4 edition = "2021" 5 rust-version = "1.83" 6 authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] ··· 16 name = "atproto-identity-resolve" 17 test = false 18 bench = false 19 20 [dependencies] 21 anyhow = "1.0" ··· 27 reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 28 tracing = { version = "0.1", features = ["async-await"] } 29 tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread"] }
··· 1 [package] 2 name = "atproto-identity" 3 + version = "0.2.0" 4 edition = "2021" 5 rust-version = "1.83" 6 authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] ··· 16 name = "atproto-identity-resolve" 17 test = false 18 bench = false 19 + doc = true 20 + 21 + [[bin]] 22 + name = "atproto-identity-sign" 23 + test = false 24 + bench = false 25 + doc = true 26 + 27 + [[bin]] 28 + name = "atproto-identity-validate" 29 + test = false 30 + bench = false 31 + doc = true 32 33 [dependencies] 34 anyhow = "1.0" ··· 40 reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 41 tracing = { version = "0.1", features = ["async-await"] } 42 tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread"] } 43 + multibase = "0.9.1" 44 + ecdsa = { version = "0.16.9", features = ["std"] } 45 + k256 = "0.13.4" 46 + p256 = "0.13.2" 47 + serde_ipld_dagcbor = "0.6.3"
+30 -1
README.md
··· 14 - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 - **Configurable DNS**: Custom DNS nameserver support with fallback to system defaults 17 - **Structured Logging**: Built-in tracing support for debugging and monitoring 18 - **Type Safety**: Comprehensive error handling with structured error types 19 ··· 29 30 ```toml 31 [dependencies] 32 - atproto-identity = "0.1.0" 33 ``` 34 35 ## Usage ··· 94 } 95 ``` 96 97 ### Configuration 98 99 The library supports various configuration options through environment variables: ··· 138 - **validation**: Input validation for handles and DIDs 139 - **config**: Configuration management and environment variable handling 140 - **errors**: Structured error types following project conventions 141 142 ## Error Handling 143
··· 14 - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 - **Configurable DNS**: Custom DNS nameserver support with fallback to system defaults 17 + - **Cryptographic Key Operations**: Support for P-256 and K-256 key identification, signature validation, and signing 18 - **Structured Logging**: Built-in tracing support for debugging and monitoring 19 - **Type Safety**: Comprehensive error handling with structured error types 20 ··· 30 31 ```toml 32 [dependencies] 33 + atproto-identity = "0.2.0" 34 ``` 35 36 ## Usage ··· 95 } 96 ``` 97 98 + ### Cryptographic Key Operations 99 + 100 + The `key` module provides utilities for working with cryptographic keys: 101 + 102 + ```rust 103 + use atproto_identity::key::{identify_key, validate, KeyType}; 104 + 105 + fn main() -> Result<(), Box<dyn std::error::Error>> { 106 + // Identify a key from a DID key string 107 + let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 108 + 109 + match key_data.0 { 110 + KeyType::K256Public => println!("K-256 public key"), 111 + KeyType::P256Public => println!("P-256 public key"), 112 + KeyType::K256Private => println!("K-256 private key"), 113 + KeyType::P256Private => println!("P-256 private key"), 114 + } 115 + 116 + // Validate a signature (example with dummy data) 117 + let content = b"hello world"; 118 + let signature = vec![0u8; 64]; // Replace with actual signature 119 + validate(&key_data, &signature, content)?; 120 + 121 + Ok(()) 122 + } 123 + ``` 124 + 125 ### Configuration 126 127 The library supports various configuration options through environment variables: ··· 166 - **validation**: Input validation for handles and DIDs 167 - **config**: Configuration management and environment variable handling 168 - **errors**: Structured error types following project conventions 169 + - **key**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves 170 171 ## Error Handling 172
+59 -3
src/bin/atproto-identity-resolve.rs
··· 8 web::query as web_query, 9 }; 10 11 #[tokio::main] 12 async fn main() -> Result<()> { 13 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); ··· 62 continue; 63 } 64 65 - let did_document = match parse_input(&resolved_did) { 66 - Ok(InputType::Plc(did)) => plc_query(&http_client, &plc_hostname, &did).await, 67 - Ok(InputType::Web(did)) => web_query(&http_client, &did).await, 68 Ok(InputType::Handle(_)) => { 69 eprintln!("error: subject resolved to handle"); 70 continue;
··· 8 web::query as web_query, 9 }; 10 11 + /// AT Protocol Identity Resolution CLI 12 + /// 13 + /// A command-line tool for resolving AT Protocol handles and DIDs to their canonical 14 + /// DID identifiers and optionally retrieving their full DID documents. Supports 15 + /// both did:plc and did:web methods with configurable DNS and HTTP settings. 16 + /// 17 + /// ## Usage 18 + /// 19 + /// Resolve one or more handles or DIDs: 20 + /// ```bash 21 + /// cargo run --bin atproto-identity-resolve alice.bsky.social 22 + /// cargo run --bin atproto-identity-resolve did:plc:ewvi7nxzyoun6zhxrhs64oiz 23 + /// cargo run --bin atproto-identity-resolve alice.bsky.social bob.example.com 24 + /// ``` 25 + /// 26 + /// Resolve and fetch DID documents: 27 + /// ```bash 28 + /// cargo run --bin atproto-identity-resolve --did-document alice.bsky.social 29 + /// cargo run --bin atproto-identity-resolve --did-document did:web:example.com 30 + /// ``` 31 + /// 32 + /// ## Arguments 33 + /// 34 + /// - `[SUBJECTS]...` - One or more AT Protocol handles or DIDs to resolve 35 + /// - `--did-document` - Additionally fetch and display the full DID document 36 + /// 37 + /// ## Environment Variables 38 + /// 39 + /// - `PLC_HOSTNAME` - PLC directory hostname (default: "plc.directory") 40 + /// - `USER_AGENT` - HTTP user agent string (default: auto-generated) 41 + /// - `CERTIFICATE_BUNDLES` - Colon-separated paths to additional CA certificates 42 + /// - `DNS_NAMESERVERS` - Comma-separated DNS nameserver addresses 43 + /// 44 + /// ## Examples 45 + /// 46 + /// Basic handle resolution: 47 + /// ```bash 48 + /// $ cargo run --bin atproto-identity-resolve alice.bsky.social 49 + /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 50 + /// ``` 51 + /// 52 + /// Resolve multiple subjects: 53 + /// ```bash 54 + /// $ cargo run --bin atproto-identity-resolve alice.bsky.social bob.example.com 55 + /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 56 + /// did:web:bob.example.com 57 + /// ``` 58 + /// 59 + /// Get DID document: 60 + /// ```bash 61 + /// $ cargo run --bin atproto-identity-resolve --did-document alice.bsky.social 62 + /// did:plc:ewvi7nxzyoun6zhxrhs64oiz 63 + /// PlcDocument { did: "did:plc:ewvi7nxzyoun6zhxrhs64oiz", ... } 64 + /// ``` 65 #[tokio::main] 66 async fn main() -> Result<()> { 67 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); ··· 116 continue; 117 } 118 119 + let did_document: Result<_, anyhow::Error> = match parse_input(&resolved_did) { 120 + Ok(InputType::Plc(did)) => plc_query(&http_client, &plc_hostname, &did) 121 + .await 122 + .map_err(Into::into), 123 + Ok(InputType::Web(did)) => web_query(&http_client, &did).await.map_err(Into::into), 124 Ok(InputType::Handle(_)) => { 125 eprintln!("error: subject resolved to handle"); 126 continue;
+101
src/bin/atproto-identity-sign.rs
···
··· 1 + use anyhow::{anyhow, Context, Result}; 2 + use atproto_identity::key::{identify_key, sign}; 3 + use std::{env, fs::File, io::BufReader}; 4 + 5 + use std::process::ExitCode; 6 + 7 + async fn real_main() -> Result<()> { 8 + let mut arguments = env::args().skip(1); //. .collect::<Vec<String>>(); 9 + 10 + if arguments.len() != 2 { 11 + return Err(anyhow!("Usage: atproto-identity-sign [did-key] [file]")); 12 + } 13 + 14 + let key_data = arguments 15 + .next() 16 + .ok_or(anyhow!("missing did-method-key value"))?; 17 + let identified_key = identify_key(&key_data)?; 18 + 19 + let json_file_path = arguments.next().ok_or(anyhow!("missing json-file value"))?; 20 + let record: serde_json::Value = File::open(json_file_path) 21 + .context("failed to open json file") 22 + .map(BufReader::new) 23 + .and_then(|value| serde_json::from_reader(value).context("failed to parse json file"))?; 24 + 25 + let serialized_record = serde_ipld_dagcbor::to_vec(&record)?; 26 + 27 + let signature = sign(&identified_key, &serialized_record)?; 28 + let encoded_signature = multibase::encode(multibase::Base::Base64Url, &signature); 29 + 30 + println!("{encoded_signature}"); 31 + 32 + Ok(()) 33 + } 34 + 35 + /// AT Protocol Digital Signature CLI 36 + /// 37 + /// A command-line tool for creating cryptographic signatures of JSON data using 38 + /// AT Protocol DID keys. Takes a JSON file, serializes it using IPLD DAG-CBOR 39 + /// format, and produces a multibase-encoded signature using the specified key. 40 + /// Supports both P-256 and K-256 elliptic curve keys via did:key method. 41 + /// 42 + /// ## Usage 43 + /// 44 + /// ```bash 45 + /// cargo run --bin atproto-identity-sign [DID_KEY] [JSON_FILE] 46 + /// ``` 47 + /// 48 + /// ## Arguments 49 + /// 50 + /// - `DID_KEY` - A did:key identifier containing the signing key (e.g., did:key:z42tv1pb3...) 51 + /// - `JSON_FILE` - Path to a JSON file containing the data to sign 52 + /// 53 + /// ## Process 54 + /// 55 + /// 1. Parses and validates the provided DID key 56 + /// 2. Reads and parses the JSON file 57 + /// 3. Serializes the JSON data using IPLD DAG-CBOR encoding 58 + /// 4. Creates a cryptographic signature using the specified key 59 + /// 5. Encodes the signature using multibase Base64URL format 60 + /// 6. Outputs the encoded signature to stdout 61 + /// 62 + /// ## Examples 63 + /// 64 + /// Create a sample JSON file and sign it: 65 + /// ```bash 66 + /// # Create test data 67 + /// $ echo '{"message": "hello world", "timestamp": "2024-01-01T00:00:00Z"}' > data.json 68 + /// 69 + /// # Sign the data 70 + /// $ cargo run --bin atproto-identity-sign did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW data.json 71 + /// uEiABC123...XYZ789 72 + /// ``` 73 + /// 74 + /// Using jo to create JSON and sign in one command: 75 + /// ```bash 76 + /// $ jo message="hello world" when="$(date)" > message.json 77 + /// $ cargo run --bin atproto-identity-sign did:key:z42tv1pb3... message.json 78 + /// uEiBDE456...ABC123 79 + /// ``` 80 + /// 81 + /// ## Supported Key Types 82 + /// 83 + /// - **P-256 (secp256r1)** - NIST standard elliptic curve 84 + /// - **K-256 (secp256k1)** - Bitcoin/Ethereum compatible curve 85 + /// 86 + /// ## Error Handling 87 + /// 88 + /// - Invalid DID key format or unsupported key type 89 + /// - Missing or unreadable JSON file 90 + /// - Malformed JSON content 91 + /// - Cryptographic signing failures 92 + /// 93 + /// All errors are reported to stderr with descriptive messages. 94 + #[tokio::main] 95 + async fn main() -> ExitCode { 96 + if let Err(err) = real_main().await { 97 + eprintln!("{err}"); 98 + return ExitCode::from(1); 99 + } 100 + ExitCode::from(0) 101 + }
+119
src/bin/atproto-identity-validate.rs
···
··· 1 + use anyhow::{anyhow, Context, Result}; 2 + use atproto_identity::key::{identify_key, validate}; 3 + use std::{env, fs::File, io::BufReader}; 4 + 5 + use std::process::ExitCode; 6 + 7 + async fn real_main() -> Result<()> { 8 + let mut arguments = env::args().skip(1); //. .collect::<Vec<String>>(); 9 + 10 + if arguments.len() != 3 { 11 + return Err(anyhow!( 12 + "Usage: atproto-identity-validate [did-key] [file] [signature]" 13 + )); 14 + } 15 + 16 + let key_data = arguments 17 + .next() 18 + .ok_or(anyhow!("missing did-method-key value"))?; 19 + let identified_key = identify_key(&key_data)?; 20 + 21 + let json_file_path = arguments.next().ok_or(anyhow!("missing json-file value"))?; 22 + let record: serde_json::Value = File::open(json_file_path) 23 + .context("failed to open json file") 24 + .map(BufReader::new) 25 + .and_then(|value| serde_json::from_reader(value).context("failed to parse json file"))?; 26 + 27 + let serialized_record = serde_ipld_dagcbor::to_vec(&record)?; 28 + 29 + let encoded_signature = arguments.next().ok_or(anyhow!("missing signature value"))?; 30 + 31 + let (_, signature_data) = multibase::decode(encoded_signature)?; 32 + 33 + validate(&identified_key, &signature_data, &serialized_record)?; 34 + 35 + Ok(()) 36 + } 37 + 38 + /// AT Protocol Signature Validation CLI 39 + /// 40 + /// A command-line tool for verifying cryptographic signatures of JSON data using 41 + /// AT Protocol DID keys. Takes a JSON file, a multibase-encoded signature, and 42 + /// a DID key, then validates that the signature was created by the specified key 43 + /// for the given data. Uses IPLD DAG-CBOR serialization format for verification. 44 + /// Supports both P-256 and K-256 elliptic curve keys via did:key method. 45 + /// 46 + /// ## Usage 47 + /// 48 + /// ```bash 49 + /// cargo run --bin atproto-identity-validate [DID_KEY] [JSON_FILE] [SIGNATURE] 50 + /// ``` 51 + /// 52 + /// ## Arguments 53 + /// 54 + /// - `DID_KEY` - A did:key identifier containing the verification key (e.g., did:key:z42tv1pb3...) 55 + /// - `JSON_FILE` - Path to a JSON file containing the original signed data 56 + /// - `SIGNATURE` - Multibase-encoded signature to validate (typically Base64URL) 57 + /// 58 + /// ## Process 59 + /// 60 + /// 1. Parses and validates the provided DID key 61 + /// 2. Reads and parses the JSON file 62 + /// 3. Serializes the JSON data using IPLD DAG-CBOR encoding (same as signing) 63 + /// 4. Decodes the multibase-encoded signature 64 + /// 5. Verifies the signature against the data using the specified key 65 + /// 6. Outputs "OK" on successful validation, error message on failure 66 + /// 7. Returns exit code 0 for success, 1 for failure 67 + /// 68 + /// ## Examples 69 + /// 70 + /// Basic signature validation workflow: 71 + /// ```bash 72 + /// # Create and sign data 73 + /// $ echo '{"message": "hello world", "timestamp": "2024-01-01T00:00:00Z"}' > data.json 74 + /// $ SIGNATURE=$(cargo run --bin atproto-identity-sign did:key:z42tv1pb3... data.json) 75 + /// 76 + /// # Validate the signature 77 + /// $ cargo run --bin atproto-identity-validate did:key:z42tv1pb3... data.json $SIGNATURE 78 + /// OK 79 + /// ``` 80 + /// 81 + /// Validating a signature from external source: 82 + /// ```bash 83 + /// $ cargo run --bin atproto-identity-validate \ 84 + /// did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \ 85 + /// message.json \ 86 + /// uEiABC123defGHI456jklMNO789pqrSTU012vwxYZ345abc 87 + /// OK 88 + /// ``` 89 + /// 90 + /// Invalid signature example: 91 + /// ```bash 92 + /// $ cargo run --bin atproto-identity-validate did:key:z42tv1pb3... data.json invalid_sig 93 + /// error-atproto-identity-key-1 Signature validation failed 94 + /// ``` 95 + /// 96 + /// ## Supported Key Types 97 + /// 98 + /// - **P-256 (secp256r1)** - NIST standard elliptic curve 99 + /// - **K-256 (secp256k1)** - Bitcoin/Ethereum compatible curve 100 + /// 101 + /// ## Error Handling 102 + /// 103 + /// - Invalid DID key format or unsupported key type 104 + /// - Missing or unreadable JSON file 105 + /// - Malformed JSON content 106 + /// - Invalid multibase-encoded signature 107 + /// - Signature validation failure (wrong key, tampered data, etc.) 108 + /// 109 + /// All errors are reported to stderr with descriptive messages and return exit code 1. 110 + /// Successful validation prints "OK" to stdout and returns exit code 0. 111 + #[tokio::main] 112 + async fn main() -> ExitCode { 113 + if let Err(err) = real_main().await { 114 + eprintln!("{err}"); 115 + return ExitCode::from(1); 116 + } 117 + println!("OK"); 118 + ExitCode::from(0) 119 + }
+6
src/config.rs
··· 1 use crate::errors::ConfigError; 2 use anyhow::Result; 3
··· 1 + //! Configuration management for AT Protocol identity operations. 2 + //! 3 + //! Provides utilities for reading environment variables, parsing configuration values, 4 + //! and managing DNS nameservers and TLS certificate bundles. Supports fallback defaults 5 + //! and structured error handling for missing or invalid configuration. 6 + 7 use crate::errors::ConfigError; 8 use anyhow::Result; 9
+49 -7
src/errors.rs
··· 21 #[derive(Debug, Error)] 22 pub enum WebDIDError { 23 /// Occurs when the DID is missing the 'did:web:' prefix 24 - #[error("error-did-web-1 Invalid DID format: missing 'did:web:' prefix")] 25 InvalidDIDPrefix, 26 27 /// Occurs when the DID is missing a hostname component 28 - #[error("error-did-web-2 Invalid DID format: missing hostname component")] 29 MissingHostname, 30 31 /// Occurs when the HTTP request to fetch the DID document fails 32 - #[error("error-did-web-3 HTTP request failed: {url} {error}")] 33 HttpRequestFailed { 34 /// The URL that was requested 35 url: String, ··· 38 }, 39 40 /// Occurs when the DID document cannot be parsed from the HTTP response 41 - #[error("error-did-web-4 Failed to parse DID document: {url} {error}")] 42 DocumentParseFailed { 43 /// The URL that was requested 44 url: String, ··· 52 pub enum ConfigError { 53 /// Occurs when a required environment variable is not set 54 #[error("error-atproto-identity-config-1 Required environment variable not found: {name}")] 55 - MissingEnvironmentVariable { name: String }, 56 57 /// Occurs when parsing an IP address from nameserver configuration fails 58 #[error("error-atproto-identity-config-2 Unable to parse nameserver IP: {value}")] 59 - InvalidNameserverIP { value: String }, 60 61 /// Occurs when version information cannot be determined 62 #[error("error-atproto-identity-config-3 Version information not available: GIT_HASH or CARGO_PKG_VERSION must be set")] ··· 91 InvalidHTTPResolutionResponse, 92 93 /// Occurs when input cannot be parsed as a valid handle or DID 94 - #[error("error-atproto-identity-resolve-8 Invalid input")] 95 InvalidInput, 96 } 97 ··· 116 error: reqwest::Error, 117 }, 118 }
··· 21 #[derive(Debug, Error)] 22 pub enum WebDIDError { 23 /// Occurs when the DID is missing the 'did:web:' prefix 24 + #[error("error-atproto-identity-web-1 Invalid DID format: missing 'did:web:' prefix")] 25 InvalidDIDPrefix, 26 27 /// Occurs when the DID is missing a hostname component 28 + #[error("error-atproto-identity-web-2 Invalid DID format: missing hostname component")] 29 MissingHostname, 30 31 /// Occurs when the HTTP request to fetch the DID document fails 32 + #[error("error-atproto-identity-web-3 HTTP request failed: {url} {error}")] 33 HttpRequestFailed { 34 /// The URL that was requested 35 url: String, ··· 38 }, 39 40 /// Occurs when the DID document cannot be parsed from the HTTP response 41 + #[error("error-atproto-identity-web-4 Failed to parse DID document: {url} {error}")] 42 DocumentParseFailed { 43 /// The URL that was requested 44 url: String, ··· 52 pub enum ConfigError { 53 /// Occurs when a required environment variable is not set 54 #[error("error-atproto-identity-config-1 Required environment variable not found: {name}")] 55 + MissingEnvironmentVariable { 56 + /// The name of the missing environment variable 57 + name: String, 58 + }, 59 60 /// Occurs when parsing an IP address from nameserver configuration fails 61 #[error("error-atproto-identity-config-2 Unable to parse nameserver IP: {value}")] 62 + InvalidNameserverIP { 63 + /// The invalid IP address value that could not be parsed 64 + value: String, 65 + }, 66 67 /// Occurs when version information cannot be determined 68 #[error("error-atproto-identity-config-3 Version information not available: GIT_HASH or CARGO_PKG_VERSION must be set")] ··· 97 InvalidHTTPResolutionResponse, 98 99 /// Occurs when input cannot be parsed as a valid handle or DID 100 + #[error("error-atproto-identity-resolve-7 Invalid input")] 101 InvalidInput, 102 } 103 ··· 122 error: reqwest::Error, 123 }, 124 } 125 + 126 + /// Error types that can occur when working with cryptographic keys 127 + #[derive(Debug, Error)] 128 + pub enum KeyError { 129 + /// Occurs when multibase decoding of a key fails 130 + #[error("error-atproto-identity-key-1 Error decoding key: {0:?}")] 131 + DecodeError(multibase::Error), 132 + 133 + /// Occurs when a key cannot be identified or is in an invalid format 134 + #[error("error-atproto-identity-key-2 Invalid key: {0:?}")] 135 + InvalidKey(anyhow::Error), 136 + 137 + /// Occurs when ECDSA signature parsing fails 138 + #[error("error-atproto-identity-key-3 Signature parsing failed: {0:?}")] 139 + SignatureError(ecdsa::signature::Error), 140 + 141 + /// Occurs when P-256 key operations fail 142 + #[error("error-atproto-identity-key-4 P256 Error: {0:?}")] 143 + P256Error(p256::ecdsa::Error), 144 + 145 + /// Occurs when K-256 key operations fail 146 + #[error("error-atproto-identity-key-5 K256 Error: {0:?}")] 147 + K256Error(k256::ecdsa::Error), 148 + 149 + /// Occurs when ECDSA cryptographic operations fail 150 + #[error("error-atproto-identity-key-6 ECDSA Error: {0:?}")] 151 + ECDSAError(ecdsa::Error), 152 + 153 + /// Occurs when secret key parsing or operations fail 154 + #[error("error-atproto-identity-key-7 Secret Key Error: {0:?}")] 155 + SecretKeyError(ecdsa::elliptic_curve::Error), 156 + 157 + /// Occurs when attempting to sign content with a public key instead of a private key 158 + #[error("error-atproto-identity-key-8 Private key required for signature")] 159 + PrivateKeyRequiredForSignature, 160 + }
+283
src/key.rs
···
··· 1 + //! Cryptographic key operations for AT Protocol identity management. 2 + //! 3 + //! This module provides utilities for working with elliptic curve cryptographic keys 4 + //! used in AT Protocol DID documents and identity verification. Supports both P-256 5 + //! (secp256r1) and K-256 (secp256k1) curves with operations for key identification, 6 + //! signature validation, and content signing. 7 + //! 8 + //! # Supported Key Types 9 + //! 10 + //! - **P-256** (secp256r1/ES256): NIST standard curve, commonly used in web standards 11 + //! - **K-256** (secp256k1/ES256K): Bitcoin curve, widely used in blockchain applications 12 + //! 13 + //! # Key Operations 14 + //! 15 + //! - Key type identification from multibase-encoded DID key strings 16 + //! - ECDSA signature validation for both public and private keys 17 + //! - Content signing with private keys 18 + //! - DID key method prefix handling 19 + //! 20 + //! # Example 21 + //! 22 + //! ```rust 23 + //! use atproto_identity::key::{identify_key, validate, KeyType}; 24 + //! 25 + //! # fn main() -> Result<(), Box<dyn std::error::Error>> { 26 + //! let key_data = identify_key("did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA")?; 27 + //! assert_eq!(key_data.0, KeyType::K256Public); 28 + //! # Ok(()) 29 + //! # } 30 + //! ``` 31 + 32 + use anyhow::{anyhow, Result}; 33 + use ecdsa::signature::Signer; 34 + 35 + use crate::errors::KeyError; 36 + 37 + /// Cryptographic key types supported for AT Protocol identity. 38 + #[derive(Debug, PartialEq)] 39 + pub enum KeyType { 40 + /// A p256 (P-256 / secp256r1 / ES256) public key. 41 + /// The multibase / multicodec prefix is 8024. 42 + P256Public, 43 + 44 + /// A p256 (P-256 / secp256r1 / ES256) private key. 45 + /// The multibase / multicodec prefix is 8626. 46 + P256Private, 47 + 48 + /// A k256 (K-256 / secp256k1 / ES256K) public key. 49 + /// The multibase / multicodec prefix is e701. 50 + K256Public, 51 + 52 + /// A k256 (K-256 / secp256k1 / ES256K) private key. 53 + /// The multibase / multicodec prefix is 8126. 54 + K256Private, 55 + } 56 + 57 + /// DID key method prefix. 58 + const DID_METHOD_KEY_PREFIX: &str = "did:key:"; 59 + 60 + /// Extracts the value portion from a DID key string. 61 + /// 62 + /// Removes the "did:key:" prefix if present, otherwise returns the original string. 63 + pub fn did_method_key_value(key: &str) -> &str { 64 + match key.strip_prefix(DID_METHOD_KEY_PREFIX) { 65 + Some(value) => value, 66 + None => key, 67 + } 68 + } 69 + 70 + /// Identifies the key type and extracts the key data from a multibase-encoded key. 71 + /// 72 + /// Returns a tuple containing the key type and the raw key bytes. 73 + pub fn identify_key(key: &str) -> Result<(KeyType, Vec<u8>), KeyError> { 74 + let stripped_key = did_method_key_value(key); 75 + let (_, decoded_multibase_key) = 76 + multibase::decode(stripped_key).map_err(KeyError::DecodeError)?; 77 + 78 + if decoded_multibase_key.len() < 3 { 79 + return Err(KeyError::InvalidKey(anyhow!("unidentified key type"))); 80 + } 81 + 82 + // These values were verified using the following method: 83 + // 84 + // 1. Use goat to generate p256 and k256 keys to sample. 85 + // `goat key generate -t k256` 86 + // 87 + // 2. Use `multibase` and `xxd` to view the hex output 88 + // `multibase decode zQ3shj41kYrAKpgMvWFZ8L4uFhQ6P57zpiQEuvL1LWWa8sZqN | xxd` 89 + // 90 + // See also: https://github.com/bluesky-social/indigo/tree/main/cmd/goat 91 + // See also: https://github.com/docknetwork/multibase-cli 92 + 93 + match &decoded_multibase_key[..2] { 94 + // P-256 / secp256r1 / ES256 private key 95 + [0x86, 0x26] => Ok((KeyType::P256Private, decoded_multibase_key[2..].to_vec())), 96 + 97 + // P-256 / secp256r1 / ES256 public key 98 + [0x80, 0x24] => Ok((KeyType::P256Public, decoded_multibase_key[2..].to_vec())), 99 + 100 + // K-256 / secp256k1 / ES256K private key 101 + [0x81, 0x26] => Ok((KeyType::K256Private, decoded_multibase_key[2..].to_vec())), 102 + 103 + // K-256 / secp256k1 / ES256K public key 104 + [0xe7, 0x01] => Ok((KeyType::K256Public, decoded_multibase_key[2..].to_vec())), 105 + 106 + _ => Err(KeyError::InvalidKey(anyhow!( 107 + "invalid multibase key type: {:?}", 108 + &decoded_multibase_key[..2] 109 + ))), 110 + } 111 + } 112 + 113 + /// Validates a signature against content using the provided key. 114 + /// 115 + /// Supports both public and private keys for signature verification. 116 + pub fn validate( 117 + key_data: &(KeyType, Vec<u8>), 118 + signature: &[u8], 119 + content: &[u8], 120 + ) -> Result<(), KeyError> { 121 + match key_data.0 { 122 + KeyType::P256Public => { 123 + let signature = 124 + ecdsa::Signature::from_slice(signature).map_err(KeyError::SignatureError)?; 125 + let verifying_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&key_data.1) 126 + .map_err(KeyError::P256Error)?; 127 + ecdsa::signature::Verifier::verify(&verifying_key, content, &signature) 128 + .map_err(KeyError::ECDSAError) 129 + } 130 + KeyType::K256Public => { 131 + let signature = 132 + ecdsa::Signature::from_slice(signature).map_err(KeyError::SignatureError)?; 133 + let verifying_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&key_data.1) 134 + .map_err(KeyError::P256Error)?; 135 + ecdsa::signature::Verifier::verify(&verifying_key, content, &signature) 136 + .map_err(KeyError::ECDSAError) 137 + } 138 + KeyType::P256Private => { 139 + let signature = 140 + ecdsa::Signature::from_slice(signature).map_err(KeyError::SignatureError)?; 141 + let secret_key: p256::SecretKey = 142 + ecdsa::elliptic_curve::SecretKey::from_slice(&key_data.1) 143 + .map_err(KeyError::SecretKeyError)?; 144 + let public_key = secret_key.public_key(); 145 + let verifying_key = p256::ecdsa::VerifyingKey::from(public_key); 146 + ecdsa::signature::Verifier::verify(&verifying_key, content, &signature) 147 + .map_err(KeyError::ECDSAError) 148 + } 149 + KeyType::K256Private => { 150 + let signature = 151 + ecdsa::Signature::from_slice(signature).map_err(KeyError::SignatureError)?; 152 + let secret_key: k256::SecretKey = 153 + ecdsa::elliptic_curve::SecretKey::from_slice(&key_data.1) 154 + .map_err(KeyError::SecretKeyError)?; 155 + let public_key = secret_key.public_key(); 156 + let verifying_key = k256::ecdsa::VerifyingKey::from(public_key); 157 + ecdsa::signature::Verifier::verify(&verifying_key, content, &signature) 158 + .map_err(KeyError::ECDSAError) 159 + } 160 + } 161 + } 162 + 163 + /// Signs content using a private key. 164 + /// 165 + /// Returns an error if a public key is provided instead of a private key. 166 + pub fn sign(key_data: &(KeyType, Vec<u8>), content: &[u8]) -> Result<Vec<u8>, KeyError> { 167 + match key_data.0 { 168 + KeyType::K256Public | KeyType::P256Public => Err(KeyError::PrivateKeyRequiredForSignature), 169 + KeyType::P256Private => { 170 + let secret_key: p256::SecretKey = 171 + ecdsa::elliptic_curve::SecretKey::from_slice(&key_data.1) 172 + .map_err(KeyError::SecretKeyError)?; 173 + let signing_key: p256::ecdsa::SigningKey = p256::ecdsa::SigningKey::from(secret_key); 174 + let signature: p256::ecdsa::Signature = signing_key 175 + .try_sign(content) 176 + .map_err(KeyError::ECDSAError)?; 177 + Ok(signature.to_vec()) 178 + } 179 + KeyType::K256Private => { 180 + let secret_key: k256::SecretKey = 181 + ecdsa::elliptic_curve::SecretKey::from_slice(&key_data.1) 182 + .map_err(KeyError::SecretKeyError)?; 183 + let signing_key: k256::ecdsa::SigningKey = k256::ecdsa::SigningKey::from(secret_key); 184 + let signature: k256::ecdsa::Signature = signing_key 185 + .try_sign(content) 186 + .map_err(KeyError::ECDSAError)?; 187 + Ok(signature.to_vec()) 188 + } 189 + } 190 + } 191 + 192 + #[cfg(test)] 193 + mod tests { 194 + use super::*; 195 + 196 + #[test] 197 + fn test_identify_key() { 198 + assert!(matches!( 199 + identify_key("z3vLVqpQveB3w8G6MQsLVseJ1Z2E1JyQzUj6WgRYNNwB9jdE"), 200 + Ok((KeyType::K256Private, _)) 201 + )); 202 + assert!(matches!( 203 + identify_key("z3vLVqpQveB3w8G6MQsLVseJ1Z2E1JyQzUj6WgRYNNwB9jdE"), 204 + Ok((KeyType::K256Private, _)) 205 + )); 206 + assert!(matches!( 207 + identify_key("z3vLVqpQveB3w8G6MQsLVseJ1Z2E1JyQzUj6WgRYNNwB9jdE"), 208 + Ok((KeyType::K256Private, _)) 209 + )); 210 + assert!(matches!( 211 + identify_key("z3vLVqpQveB3w8G6MQsLVseJ1Z2E1JyQzUj6WgRYNNwB9jdE"), 212 + Ok((KeyType::K256Private, _)) 213 + )); 214 + assert!(matches!( 215 + identify_key("asdasdasd"), 216 + Err(KeyError::DecodeError(_)) 217 + )); 218 + assert!(matches!( 219 + identify_key("z4vLVqpQveB3w8G6MQsLVseJ1Z2E1JyQzUj6WgRYNNwB9jdE"), 220 + Err(KeyError::InvalidKey(_)) 221 + )); 222 + } 223 + 224 + #[test] 225 + fn test_sign_p256() -> Result<()> { 226 + let private_key = "did:key:z42tnbHmmnhF11nwSnp5kQJbcZQw2Vbw5WF3ABDSxPtDgU2o"; 227 + let public_key = "did:key:zDnaeXduWbJ1b1Kgjf3uCdCpMDF1LEDizUiyxAxGwerou3Nh2"; 228 + 229 + let private_key_data = identify_key(private_key); 230 + assert!(matches!(private_key_data, Ok((KeyType::P256Private, _)))); 231 + let private_key_data = private_key_data.unwrap(); 232 + 233 + let public_key_data = identify_key(public_key); 234 + assert!(matches!(public_key_data, Ok((KeyType::P256Public, _)))); 235 + let public_key_data = public_key_data.unwrap(); 236 + 237 + let content = "hello world".as_bytes(); 238 + 239 + let signature = sign(&private_key_data, content); 240 + assert!(signature.is_ok()); 241 + let signature = signature.unwrap(); 242 + 243 + { 244 + let validation = validate(&public_key_data, &signature, content); 245 + assert!(validation.is_ok()); 246 + } 247 + { 248 + let validation = validate(&private_key_data, &signature, content); 249 + assert!(validation.is_ok()); 250 + } 251 + Ok(()) 252 + } 253 + 254 + #[test] 255 + fn test_sign_k256() -> Result<()> { 256 + let private_key = "did:key:z3vLY4nbXy2rV4Qr65gUtfnSF3A8Be7gmYzUiCX6eo2PR1Rt"; 257 + let public_key = "did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA"; 258 + 259 + let private_key_data = identify_key(private_key); 260 + assert!(matches!(private_key_data, Ok((KeyType::K256Private, _)))); 261 + let private_key_data = private_key_data.unwrap(); 262 + 263 + let public_key_data = identify_key(public_key); 264 + assert!(matches!(public_key_data, Ok((KeyType::K256Public, _)))); 265 + let public_key_data = public_key_data.unwrap(); 266 + 267 + let content = "hello world".as_bytes(); 268 + 269 + let signature = sign(&private_key_data, content); 270 + assert!(signature.is_ok()); 271 + let signature = signature.unwrap(); 272 + 273 + { 274 + let validation = validate(&public_key_data, &signature, content); 275 + assert!(validation.is_ok()); 276 + } 277 + { 278 + let validation = validate(&private_key_data, &signature, content); 279 + assert!(validation.is_ok()); 280 + } 281 + Ok(()) 282 + } 283 + }
+2
src/lib.rs
··· 12 //! - [`validation`] - Input validation for handles and DIDs 13 //! - [`config`] - Configuration management and environment variable handling 14 //! - [`errors`] - Structured error types following project conventions 15 //! 16 //! ## Usage 17 //! ··· 24 25 pub mod config; 26 pub mod errors; 27 pub mod model; 28 pub mod plc; 29 pub mod resolve;
··· 12 //! - [`validation`] - Input validation for handles and DIDs 13 //! - [`config`] - Configuration management and environment variable handling 14 //! - [`errors`] - Structured error types following project conventions 15 + //! - [`key`] - Cryptographic key operations for P-256 and K-256 curves 16 //! 17 //! ## Usage 18 //! ··· 25 26 pub mod config; 27 pub mod errors; 28 + pub mod key; 29 pub mod model; 30 pub mod plc; 31 pub mod resolve;
+6
src/model.rs
··· 1 use serde::{Deserialize, Serialize}; 2 use serde_json::Value; 3 use std::collections::HashMap;
··· 1 + //! Data structures for DID documents and AT Protocol entities. 2 + //! 3 + //! Defines the core data models used throughout the AT Protocol identity system 4 + //! including DID documents, services, and verification methods. All structures 5 + //! support JSON serialization/deserialization for working with AT Protocol APIs. 6 + 7 use serde::{Deserialize, Serialize}; 8 use serde_json::Value; 9 use std::collections::HashMap;
+1 -4
src/plc.rs
··· 9 //! The primary function `query()` fetches complete DID documents from PLC directory servers 10 //! using HTTPS requests to the standard PLC API endpoint format. 11 12 - use anyhow::Result; 13 - 14 use super::errors::PLCDIDError; 15 use super::model::Document; 16 ··· 20 http_client: &reqwest::Client, 21 plc_hostname: &str, 22 did: &str, 23 - ) -> Result<Document> { 24 let url = format!("https://{}/{}", plc_hostname, did); 25 26 http_client ··· 34 .json::<Document>() 35 .await 36 .map_err(|error| PLCDIDError::DocumentParseFailed { url, error }) 37 - .map_err(Into::into) 38 }
··· 9 //! The primary function `query()` fetches complete DID documents from PLC directory servers 10 //! using HTTPS requests to the standard PLC API endpoint format. 11 12 use super::errors::PLCDIDError; 13 use super::model::Document; 14 ··· 18 http_client: &reqwest::Client, 19 plc_hostname: &str, 20 did: &str, 21 + ) -> Result<Document, PLCDIDError> { 22 let url = format!("https://{}/{}", plc_hostname, did); 23 24 http_client ··· 32 .json::<Document>() 33 .await 34 .map_err(|error| PLCDIDError::DocumentParseFailed { url, error }) 35 }
+6
src/validation.rs
··· 1 /// Maximum length for a valid hostname as defined in RFC 1035 2 const MAX_HOSTNAME_LENGTH: usize = 253; 3
··· 1 + //! Input validation for AT Protocol handles and DIDs. 2 + //! 3 + //! Provides validation functions for various identifier formats used in the AT Protocol 4 + //! ecosystem including handles, hostnames, and DID identifiers. Follows RFC standards 5 + //! for hostname validation and AT Protocol specifications for handle formats. 6 + 7 /// Maximum length for a valid hostname as defined in RFC 1035 8 const MAX_HOSTNAME_LENGTH: usize = 253; 9
+5 -6
src/web.rs
··· 15 //! Transforms DIDs like `did:web:example.com:path:subpath` into HTTPS URLs following 16 //! the did:web specification for well-known document locations. 17 18 - use anyhow::Result; 19 - 20 use super::errors::WebDIDError; 21 use super::model::Document; 22 ··· 46 47 /// Queries a did:web DID document from its hosting location. 48 /// Resolves the DID to HTTPS URL and fetches the JSON document. 49 - pub async fn query(http_client: &reqwest::Client, did: &str) -> Result<Document> { 50 let url = did_web_to_url(did)?; 51 52 http_client ··· 60 .json::<Document>() 61 .await 62 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 63 - .map_err(Into::into) 64 } 65 66 /// Queries a DID document directly from a hostname's well-known location. 67 /// Fetches from https://{hostname}/.well-known/did.json 68 - pub async fn query_hostname(http_client: &reqwest::Client, hostname: &str) -> Result<Document> { 69 let url = format!("https://{}/.well-known/did.json", hostname); 70 71 http_client ··· 79 .json::<Document>() 80 .await 81 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 82 - .map_err(Into::into) 83 } 84 85 #[cfg(test)]
··· 15 //! Transforms DIDs like `did:web:example.com:path:subpath` into HTTPS URLs following 16 //! the did:web specification for well-known document locations. 17 18 use super::errors::WebDIDError; 19 use super::model::Document; 20 ··· 44 45 /// Queries a did:web DID document from its hosting location. 46 /// Resolves the DID to HTTPS URL and fetches the JSON document. 47 + pub async fn query(http_client: &reqwest::Client, did: &str) -> Result<Document, WebDIDError> { 48 let url = did_web_to_url(did)?; 49 50 http_client ··· 58 .json::<Document>() 59 .await 60 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 61 } 62 63 /// Queries a DID document directly from a hostname's well-known location. 64 /// Fetches from https://{hostname}/.well-known/did.json 65 + pub async fn query_hostname( 66 + http_client: &reqwest::Client, 67 + hostname: &str, 68 + ) -> Result<Document, WebDIDError> { 69 let url = format!("https://{}/.well-known/did.json", hostname); 70 71 http_client ··· 79 .json::<Document>() 80 .await 81 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 82 } 83 84 #[cfg(test)]