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 20 ## Architecture 21 21 22 22 A comprehensive Rust library with: 23 - - Modular architecture with 7 core modules (resolve, plc, web, model, validation, config, errors) 23 + - Modular architecture with 8 core modules (resolve, plc, web, model, validation, config, errors, key) 24 24 - Complete CLI tool for identity resolution (`atproto-identity-resolve`) 25 25 - Rust edition 2021 with modern async/await patterns 26 26 - Comprehensive error handling with structured error types ··· 37 37 38 38 * error-atproto-identity-resolve-1 Multiple DIDs resolved for method 39 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 40 + * error-atproto-identity-key-1 Error decoding key: invalid 41 41 42 42 Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. 43 43 ··· 51 51 } 52 52 ``` 53 53 54 - ### Logging 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 55 59 56 60 Use tracing for structured logging. 57 61 ··· 67 71 - **`src/validation.rs`**: Input validation for handles and DIDs 68 72 - **`src/config.rs`**: Configuration management and environment variable handling 69 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 70 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 51 52 52 [[package]] 53 53 name = "atproto-identity" 54 - version = "0.1.1" 54 + version = "0.2.0" 55 55 dependencies = [ 56 56 "anyhow", 57 + "ecdsa", 57 58 "futures-util", 58 59 "hickory-resolver", 60 + "k256", 61 + "multibase", 62 + "p256", 59 63 "reqwest", 60 64 "serde", 65 + "serde_ipld_dagcbor", 61 66 "serde_json", 62 67 "thiserror 2.0.12", 63 68 "tokio", ··· 86 91 ] 87 92 88 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]] 89 106 name = "base64" 90 107 version = "0.22.1" 91 108 source = "registry+https://github.com/rust-lang/crates.io-index" 92 109 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 93 110 94 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]] 95 118 name = "bitflags" 96 119 version = "2.9.1" 97 120 source = "registry+https://github.com/rust-lang/crates.io-index" 98 121 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 99 122 100 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]] 101 133 name = "bumpalo" 102 134 version = "3.17.0" 103 135 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 110 142 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 111 143 112 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]] 113 154 name = "cc" 114 155 version = "1.2.24" 115 156 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 131 172 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 132 173 133 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]] 134 195 name = "core-foundation" 135 196 version = "0.9.4" 136 197 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 147 208 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 148 209 149 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]] 150 229 name = "critical-section" 151 230 version = "1.2.0" 152 231 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 177 256 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 178 257 179 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]] 180 281 name = "data-encoding" 181 282 version = "2.9.0" 182 283 source = "registry+https://github.com/rust-lang/crates.io-index" 183 284 checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 184 285 185 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]] 186 330 name = "displaydoc" 187 331 version = "0.2.5" 188 332 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 194 338 ] 195 339 196 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]] 197 375 name = "encoding_rs" 198 376 version = "0.8.35" 199 377 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 237 415 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 238 416 239 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]] 240 428 name = "fnv" 241 429 version = "1.0.7" 242 430 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 339 527 ] 340 528 341 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]] 342 541 name = "getrandom" 343 542 version = "0.2.16" 344 543 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 372 571 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 373 572 374 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]] 375 585 name = "h2" 376 586 version = "0.4.10" 377 587 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 446 656 "thiserror 2.0.12", 447 657 "tokio", 448 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", 449 668 ] 450 669 451 670 [[package]] ··· 697 916 ] 698 917 699 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]] 700 930 name = "ipnet" 701 931 version = "2.11.0" 702 932 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 729 959 ] 730 960 731 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]] 732 976 name = "lazy_static" 733 977 version = "1.5.0" 734 978 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 848 1092 ] 849 1093 850 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]] 851 1117 name = "native-tls" 852 1118 version = "0.2.14" 853 1119 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 944 1210 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 945 1211 946 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]] 947 1225 name = "parking_lot" 948 1226 version = "0.12.4" 949 1227 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 967 1245 ] 968 1246 969 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]] 970 1257 name = "percent-encoding" 971 1258 version = "2.3.1" 972 1259 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 985 1272 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 986 1273 987 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]] 988 1285 name = "pkg-config" 989 1286 version = "0.3.32" 990 1287 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1012 1309 checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1013 1310 dependencies = [ 1014 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", 1015 1321 ] 1016 1322 1017 1323 [[package]] ··· 1100 1406 checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1101 1407 dependencies = [ 1102 1408 "rand_chacha", 1103 - "rand_core", 1409 + "rand_core 0.9.3", 1104 1410 ] 1105 1411 1106 1412 [[package]] ··· 1110 1416 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1111 1417 dependencies = [ 1112 1418 "ppv-lite86", 1113 - "rand_core", 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", 1114 1429 ] 1115 1430 1116 1431 [[package]] ··· 1228 1543 checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 1229 1544 1230 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]] 1231 1556 name = "ring" 1232 1557 version = "0.17.14" 1233 1558 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1344 1669 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1345 1670 1346 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]] 1347 1686 name = "security-framework" 1348 1687 version = "2.11.1" 1349 1688 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1382 1721 ] 1383 1722 1384 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]] 1385 1733 name = "serde_derive" 1386 1734 version = "1.0.219" 1387 1735 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1393 1741 ] 1394 1742 1395 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]] 1396 1756 name = "serde_json" 1397 1757 version = "1.0.140" 1398 1758 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1417 1777 ] 1418 1778 1419 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]] 1420 1791 name = "sharded-slab" 1421 1792 version = "0.1.7" 1422 1793 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1432 1803 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1433 1804 1434 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]] 1435 1816 name = "slab" 1436 1817 version = "0.4.9" 1437 1818 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1454 1835 dependencies = [ 1455 1836 "libc", 1456 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", 1457 1848 ] 1458 1849 1459 1850 [[package]] ··· 1787 2178 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1788 2179 1789 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]] 1790 2187 name = "unicode-ident" 1791 2188 version = "1.0.18" 1792 2189 source = "registry+https://github.com/rust-lang/crates.io-index" 1793 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" 1794 2197 1795 2198 [[package]] 1796 2199 name = "untrusted" ··· 1837 2240 version = "0.2.15" 1838 2241 source = "registry+https://github.com/rust-lang/crates.io-index" 1839 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" 1840 2249 1841 2250 [[package]] 1842 2251 name = "want"
+19 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "atproto-identity" 3 - version = "0.1.1" 3 + version = "0.2.0" 4 4 edition = "2021" 5 5 rust-version = "1.83" 6 6 authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] ··· 16 16 name = "atproto-identity-resolve" 17 17 test = false 18 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 19 32 20 33 [dependencies] 21 34 anyhow = "1.0" ··· 27 40 reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 28 41 tracing = { version = "0.1", features = ["async-await"] } 29 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 14 - **DID Document Retrieval**: Fetch and parse DID documents for `did:plc` and `did:web` identifiers 15 15 - **Multiple Resolution Methods**: Supports both DNS and HTTP-based handle resolution with conflict detection 16 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 17 18 - **Structured Logging**: Built-in tracing support for debugging and monitoring 18 19 - **Type Safety**: Comprehensive error handling with structured error types 19 20 ··· 29 30 30 31 ```toml 31 32 [dependencies] 32 - atproto-identity = "0.1.0" 33 + atproto-identity = "0.2.0" 33 34 ``` 34 35 35 36 ## Usage ··· 94 95 } 95 96 ``` 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 + 97 125 ### Configuration 98 126 99 127 The library supports various configuration options through environment variables: ··· 138 166 - **validation**: Input validation for handles and DIDs 139 167 - **config**: Configuration management and environment variable handling 140 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 141 170 142 171 ## Error Handling 143 172
+59 -3
src/bin/atproto-identity-resolve.rs
··· 8 8 web::query as web_query, 9 9 }; 10 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 + /// ``` 11 65 #[tokio::main] 12 66 async fn main() -> Result<()> { 13 67 let plc_hostname = default_env("PLC_HOSTNAME", "plc.directory"); ··· 62 116 continue; 63 117 } 64 118 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, 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), 68 124 Ok(InputType::Handle(_)) => { 69 125 eprintln!("error: subject resolved to handle"); 70 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 + //! 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 + 1 7 use crate::errors::ConfigError; 2 8 use anyhow::Result; 3 9
+49 -7
src/errors.rs
··· 21 21 #[derive(Debug, Error)] 22 22 pub enum WebDIDError { 23 23 /// Occurs when the DID is missing the 'did:web:' prefix 24 - #[error("error-did-web-1 Invalid DID format: missing 'did:web:' prefix")] 24 + #[error("error-atproto-identity-web-1 Invalid DID format: missing 'did:web:' prefix")] 25 25 InvalidDIDPrefix, 26 26 27 27 /// Occurs when the DID is missing a hostname component 28 - #[error("error-did-web-2 Invalid DID format: missing hostname component")] 28 + #[error("error-atproto-identity-web-2 Invalid DID format: missing hostname component")] 29 29 MissingHostname, 30 30 31 31 /// Occurs when the HTTP request to fetch the DID document fails 32 - #[error("error-did-web-3 HTTP request failed: {url} {error}")] 32 + #[error("error-atproto-identity-web-3 HTTP request failed: {url} {error}")] 33 33 HttpRequestFailed { 34 34 /// The URL that was requested 35 35 url: String, ··· 38 38 }, 39 39 40 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}")] 41 + #[error("error-atproto-identity-web-4 Failed to parse DID document: {url} {error}")] 42 42 DocumentParseFailed { 43 43 /// The URL that was requested 44 44 url: String, ··· 52 52 pub enum ConfigError { 53 53 /// Occurs when a required environment variable is not set 54 54 #[error("error-atproto-identity-config-1 Required environment variable not found: {name}")] 55 - MissingEnvironmentVariable { name: String }, 55 + MissingEnvironmentVariable { 56 + /// The name of the missing environment variable 57 + name: String, 58 + }, 56 59 57 60 /// Occurs when parsing an IP address from nameserver configuration fails 58 61 #[error("error-atproto-identity-config-2 Unable to parse nameserver IP: {value}")] 59 - InvalidNameserverIP { value: String }, 62 + InvalidNameserverIP { 63 + /// The invalid IP address value that could not be parsed 64 + value: String, 65 + }, 60 66 61 67 /// Occurs when version information cannot be determined 62 68 #[error("error-atproto-identity-config-3 Version information not available: GIT_HASH or CARGO_PKG_VERSION must be set")] ··· 91 97 InvalidHTTPResolutionResponse, 92 98 93 99 /// Occurs when input cannot be parsed as a valid handle or DID 94 - #[error("error-atproto-identity-resolve-8 Invalid input")] 100 + #[error("error-atproto-identity-resolve-7 Invalid input")] 95 101 InvalidInput, 96 102 } 97 103 ··· 116 122 error: reqwest::Error, 117 123 }, 118 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 12 //! - [`validation`] - Input validation for handles and DIDs 13 13 //! - [`config`] - Configuration management and environment variable handling 14 14 //! - [`errors`] - Structured error types following project conventions 15 + //! - [`key`] - Cryptographic key operations for P-256 and K-256 curves 15 16 //! 16 17 //! ## Usage 17 18 //! ··· 24 25 25 26 pub mod config; 26 27 pub mod errors; 28 + pub mod key; 27 29 pub mod model; 28 30 pub mod plc; 29 31 pub mod resolve;
+6
src/model.rs
··· 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 + 1 7 use serde::{Deserialize, Serialize}; 2 8 use serde_json::Value; 3 9 use std::collections::HashMap;
+1 -4
src/plc.rs
··· 9 9 //! The primary function `query()` fetches complete DID documents from PLC directory servers 10 10 //! using HTTPS requests to the standard PLC API endpoint format. 11 11 12 - use anyhow::Result; 13 - 14 12 use super::errors::PLCDIDError; 15 13 use super::model::Document; 16 14 ··· 20 18 http_client: &reqwest::Client, 21 19 plc_hostname: &str, 22 20 did: &str, 23 - ) -> Result<Document> { 21 + ) -> Result<Document, PLCDIDError> { 24 22 let url = format!("https://{}/{}", plc_hostname, did); 25 23 26 24 http_client ··· 34 32 .json::<Document>() 35 33 .await 36 34 .map_err(|error| PLCDIDError::DocumentParseFailed { url, error }) 37 - .map_err(Into::into) 38 35 }
+6
src/validation.rs
··· 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 + 1 7 /// Maximum length for a valid hostname as defined in RFC 1035 2 8 const MAX_HOSTNAME_LENGTH: usize = 253; 3 9
+5 -6
src/web.rs
··· 15 15 //! Transforms DIDs like `did:web:example.com:path:subpath` into HTTPS URLs following 16 16 //! the did:web specification for well-known document locations. 17 17 18 - use anyhow::Result; 19 - 20 18 use super::errors::WebDIDError; 21 19 use super::model::Document; 22 20 ··· 46 44 47 45 /// Queries a did:web DID document from its hosting location. 48 46 /// Resolves the DID to HTTPS URL and fetches the JSON document. 49 - pub async fn query(http_client: &reqwest::Client, did: &str) -> Result<Document> { 47 + pub async fn query(http_client: &reqwest::Client, did: &str) -> Result<Document, WebDIDError> { 50 48 let url = did_web_to_url(did)?; 51 49 52 50 http_client ··· 60 58 .json::<Document>() 61 59 .await 62 60 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 63 - .map_err(Into::into) 64 61 } 65 62 66 63 /// Queries a DID document directly from a hostname's well-known location. 67 64 /// Fetches from https://{hostname}/.well-known/did.json 68 - pub async fn query_hostname(http_client: &reqwest::Client, hostname: &str) -> Result<Document> { 65 + pub async fn query_hostname( 66 + http_client: &reqwest::Client, 67 + hostname: &str, 68 + ) -> Result<Document, WebDIDError> { 69 69 let url = format!("https://{}/.well-known/did.json", hostname); 70 70 71 71 http_client ··· 79 79 .json::<Document>() 80 80 .await 81 81 .map_err(|error| WebDIDError::DocumentParseFailed { url, error }) 82 - .map_err(Into::into) 83 82 } 84 83 85 84 #[cfg(test)]