├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── spec.md └── src ├── base58btc.rs ├── bin └── szdt.rs ├── byte_counter_reader.rs ├── car.rs ├── car_claim_header.rs ├── cid.rs ├── claim.rs ├── did.rs ├── ed25519.rs ├── error.rs ├── file.rs ├── lib.rs ├── manifest.rs ├── multiformats.rs ├── multihash.rs ├── util.rs └── varint.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "base-x" 57 | version = "0.2.11" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 60 | 61 | [[package]] 62 | name = "base64ct" 63 | version = "1.6.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "2.9.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 72 | 73 | [[package]] 74 | name = "block-buffer" 75 | version = "0.10.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 78 | dependencies = [ 79 | "generic-array", 80 | ] 81 | 82 | [[package]] 83 | name = "bs58" 84 | version = "0.5.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" 87 | dependencies = [ 88 | "tinyvec", 89 | ] 90 | 91 | [[package]] 92 | name = "cbor4ii" 93 | version = "0.2.14" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 96 | dependencies = [ 97 | "serde", 98 | ] 99 | 100 | [[package]] 101 | name = "cfg-if" 102 | version = "1.0.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 105 | 106 | [[package]] 107 | name = "cid" 108 | version = "0.11.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 111 | dependencies = [ 112 | "core2", 113 | "multibase", 114 | "multihash", 115 | "serde", 116 | "serde_bytes", 117 | "unsigned-varint", 118 | ] 119 | 120 | [[package]] 121 | name = "clap" 122 | version = "4.5.31" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" 125 | dependencies = [ 126 | "clap_builder", 127 | "clap_derive", 128 | ] 129 | 130 | [[package]] 131 | name = "clap_builder" 132 | version = "4.5.31" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" 135 | dependencies = [ 136 | "anstream", 137 | "anstyle", 138 | "clap_lex", 139 | "strsim", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_derive" 144 | version = "4.5.28" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 147 | dependencies = [ 148 | "heck", 149 | "proc-macro2", 150 | "quote", 151 | "syn", 152 | ] 153 | 154 | [[package]] 155 | name = "clap_lex" 156 | version = "0.7.4" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 159 | 160 | [[package]] 161 | name = "colorchoice" 162 | version = "1.0.3" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 165 | 166 | [[package]] 167 | name = "const-oid" 168 | version = "0.9.6" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 171 | 172 | [[package]] 173 | name = "core2" 174 | version = "0.4.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 177 | dependencies = [ 178 | "memchr", 179 | ] 180 | 181 | [[package]] 182 | name = "cpufeatures" 183 | version = "0.2.17" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 186 | dependencies = [ 187 | "libc", 188 | ] 189 | 190 | [[package]] 191 | name = "crypto-common" 192 | version = "0.1.6" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 195 | dependencies = [ 196 | "generic-array", 197 | "typenum", 198 | ] 199 | 200 | [[package]] 201 | name = "curve25519-dalek" 202 | version = "4.1.3" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 205 | dependencies = [ 206 | "cfg-if", 207 | "cpufeatures", 208 | "curve25519-dalek-derive", 209 | "digest", 210 | "fiat-crypto", 211 | "rustc_version", 212 | "subtle", 213 | "zeroize", 214 | ] 215 | 216 | [[package]] 217 | name = "curve25519-dalek-derive" 218 | version = "0.1.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 221 | dependencies = [ 222 | "proc-macro2", 223 | "quote", 224 | "syn", 225 | ] 226 | 227 | [[package]] 228 | name = "data-encoding" 229 | version = "2.8.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" 232 | 233 | [[package]] 234 | name = "data-encoding-macro" 235 | version = "0.1.17" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558" 238 | dependencies = [ 239 | "data-encoding", 240 | "data-encoding-macro-internal", 241 | ] 242 | 243 | [[package]] 244 | name = "data-encoding-macro-internal" 245 | version = "0.1.15" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" 248 | dependencies = [ 249 | "data-encoding", 250 | "syn", 251 | ] 252 | 253 | [[package]] 254 | name = "der" 255 | version = "0.7.9" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 258 | dependencies = [ 259 | "const-oid", 260 | "zeroize", 261 | ] 262 | 263 | [[package]] 264 | name = "digest" 265 | version = "0.10.7" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 268 | dependencies = [ 269 | "block-buffer", 270 | "crypto-common", 271 | ] 272 | 273 | [[package]] 274 | name = "displaydoc" 275 | version = "0.2.5" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | "syn", 282 | ] 283 | 284 | [[package]] 285 | name = "ed25519" 286 | version = "2.2.3" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 289 | dependencies = [ 290 | "pkcs8", 291 | "serde", 292 | "signature", 293 | ] 294 | 295 | [[package]] 296 | name = "ed25519-dalek" 297 | version = "2.1.1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" 300 | dependencies = [ 301 | "curve25519-dalek", 302 | "ed25519", 303 | "rand_core", 304 | "serde", 305 | "sha2", 306 | "signature", 307 | "subtle", 308 | "zeroize", 309 | ] 310 | 311 | [[package]] 312 | name = "errno" 313 | version = "0.3.11" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 316 | dependencies = [ 317 | "libc", 318 | "windows-sys", 319 | ] 320 | 321 | [[package]] 322 | name = "fastrand" 323 | version = "2.3.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 326 | 327 | [[package]] 328 | name = "fiat-crypto" 329 | version = "0.2.9" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 332 | 333 | [[package]] 334 | name = "form_urlencoded" 335 | version = "1.2.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 338 | dependencies = [ 339 | "percent-encoding", 340 | ] 341 | 342 | [[package]] 343 | name = "generic-array" 344 | version = "0.14.7" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 347 | dependencies = [ 348 | "typenum", 349 | "version_check", 350 | ] 351 | 352 | [[package]] 353 | name = "getrandom" 354 | version = "0.2.15" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 357 | dependencies = [ 358 | "cfg-if", 359 | "libc", 360 | "wasi 0.11.0+wasi-snapshot-preview1", 361 | ] 362 | 363 | [[package]] 364 | name = "getrandom" 365 | version = "0.3.2" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 368 | dependencies = [ 369 | "cfg-if", 370 | "libc", 371 | "r-efi", 372 | "wasi 0.14.2+wasi-0.2.4", 373 | ] 374 | 375 | [[package]] 376 | name = "heck" 377 | version = "0.5.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 380 | 381 | [[package]] 382 | name = "icu_collections" 383 | version = "2.0.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 386 | dependencies = [ 387 | "displaydoc", 388 | "potential_utf", 389 | "yoke", 390 | "zerofrom", 391 | "zerovec", 392 | ] 393 | 394 | [[package]] 395 | name = "icu_locale_core" 396 | version = "2.0.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 399 | dependencies = [ 400 | "displaydoc", 401 | "litemap", 402 | "tinystr", 403 | "writeable", 404 | "zerovec", 405 | ] 406 | 407 | [[package]] 408 | name = "icu_normalizer" 409 | version = "2.0.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 412 | dependencies = [ 413 | "displaydoc", 414 | "icu_collections", 415 | "icu_normalizer_data", 416 | "icu_properties", 417 | "icu_provider", 418 | "smallvec", 419 | "zerovec", 420 | ] 421 | 422 | [[package]] 423 | name = "icu_normalizer_data" 424 | version = "2.0.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 427 | 428 | [[package]] 429 | name = "icu_properties" 430 | version = "2.0.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 433 | dependencies = [ 434 | "displaydoc", 435 | "icu_collections", 436 | "icu_locale_core", 437 | "icu_properties_data", 438 | "icu_provider", 439 | "potential_utf", 440 | "zerotrie", 441 | "zerovec", 442 | ] 443 | 444 | [[package]] 445 | name = "icu_properties_data" 446 | version = "2.0.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 449 | 450 | [[package]] 451 | name = "icu_provider" 452 | version = "2.0.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 455 | dependencies = [ 456 | "displaydoc", 457 | "icu_locale_core", 458 | "stable_deref_trait", 459 | "tinystr", 460 | "writeable", 461 | "yoke", 462 | "zerofrom", 463 | "zerotrie", 464 | "zerovec", 465 | ] 466 | 467 | [[package]] 468 | name = "idna" 469 | version = "1.0.3" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 472 | dependencies = [ 473 | "idna_adapter", 474 | "smallvec", 475 | "utf8_iter", 476 | ] 477 | 478 | [[package]] 479 | name = "idna_adapter" 480 | version = "1.2.1" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 483 | dependencies = [ 484 | "icu_normalizer", 485 | "icu_properties", 486 | ] 487 | 488 | [[package]] 489 | name = "ipld-core" 490 | version = "0.4.2" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 493 | dependencies = [ 494 | "cid", 495 | "serde", 496 | "serde_bytes", 497 | ] 498 | 499 | [[package]] 500 | name = "is_terminal_polyfill" 501 | version = "1.70.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 504 | 505 | [[package]] 506 | name = "libc" 507 | version = "0.2.170" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 510 | 511 | [[package]] 512 | name = "linux-raw-sys" 513 | version = "0.9.4" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 516 | 517 | [[package]] 518 | name = "litemap" 519 | version = "0.8.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 522 | 523 | [[package]] 524 | name = "memchr" 525 | version = "2.7.4" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 528 | 529 | [[package]] 530 | name = "multibase" 531 | version = "0.9.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 534 | dependencies = [ 535 | "base-x", 536 | "data-encoding", 537 | "data-encoding-macro", 538 | ] 539 | 540 | [[package]] 541 | name = "multihash" 542 | version = "0.19.3" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 545 | dependencies = [ 546 | "core2", 547 | "serde", 548 | "unsigned-varint", 549 | ] 550 | 551 | [[package]] 552 | name = "once_cell" 553 | version = "1.21.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" 556 | 557 | [[package]] 558 | name = "percent-encoding" 559 | version = "2.3.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 562 | 563 | [[package]] 564 | name = "pkcs8" 565 | version = "0.10.2" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 568 | dependencies = [ 569 | "der", 570 | "spki", 571 | ] 572 | 573 | [[package]] 574 | name = "potential_utf" 575 | version = "0.1.2" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 578 | dependencies = [ 579 | "zerovec", 580 | ] 581 | 582 | [[package]] 583 | name = "ppv-lite86" 584 | version = "0.2.21" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 587 | dependencies = [ 588 | "zerocopy", 589 | ] 590 | 591 | [[package]] 592 | name = "proc-macro2" 593 | version = "1.0.94" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 596 | dependencies = [ 597 | "unicode-ident", 598 | ] 599 | 600 | [[package]] 601 | name = "quote" 602 | version = "1.0.39" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 605 | dependencies = [ 606 | "proc-macro2", 607 | ] 608 | 609 | [[package]] 610 | name = "r-efi" 611 | version = "5.2.0" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 614 | 615 | [[package]] 616 | name = "rand" 617 | version = "0.8.5" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 620 | dependencies = [ 621 | "libc", 622 | "rand_chacha", 623 | "rand_core", 624 | ] 625 | 626 | [[package]] 627 | name = "rand_chacha" 628 | version = "0.3.1" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 631 | dependencies = [ 632 | "ppv-lite86", 633 | "rand_core", 634 | ] 635 | 636 | [[package]] 637 | name = "rand_core" 638 | version = "0.6.4" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 641 | dependencies = [ 642 | "getrandom 0.2.15", 643 | ] 644 | 645 | [[package]] 646 | name = "rustc_version" 647 | version = "0.4.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 650 | dependencies = [ 651 | "semver", 652 | ] 653 | 654 | [[package]] 655 | name = "rustix" 656 | version = "1.0.5" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 659 | dependencies = [ 660 | "bitflags", 661 | "errno", 662 | "libc", 663 | "linux-raw-sys", 664 | "windows-sys", 665 | ] 666 | 667 | [[package]] 668 | name = "scopeguard" 669 | version = "1.2.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 672 | 673 | [[package]] 674 | name = "semver" 675 | version = "1.0.26" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 678 | 679 | [[package]] 680 | name = "serde" 681 | version = "1.0.219" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 684 | dependencies = [ 685 | "serde_derive", 686 | ] 687 | 688 | [[package]] 689 | name = "serde_bytes" 690 | version = "0.11.17" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 693 | dependencies = [ 694 | "serde", 695 | ] 696 | 697 | [[package]] 698 | name = "serde_derive" 699 | version = "1.0.219" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 702 | dependencies = [ 703 | "proc-macro2", 704 | "quote", 705 | "syn", 706 | ] 707 | 708 | [[package]] 709 | name = "serde_ipld_dagcbor" 710 | version = "0.6.3" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 713 | dependencies = [ 714 | "cbor4ii", 715 | "ipld-core", 716 | "scopeguard", 717 | "serde", 718 | ] 719 | 720 | [[package]] 721 | name = "sha2" 722 | version = "0.10.8" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 725 | dependencies = [ 726 | "cfg-if", 727 | "cpufeatures", 728 | "digest", 729 | ] 730 | 731 | [[package]] 732 | name = "signature" 733 | version = "2.2.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 736 | dependencies = [ 737 | "digest", 738 | "rand_core", 739 | ] 740 | 741 | [[package]] 742 | name = "smallvec" 743 | version = "1.15.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 746 | 747 | [[package]] 748 | name = "spki" 749 | version = "0.7.3" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 752 | dependencies = [ 753 | "base64ct", 754 | "der", 755 | ] 756 | 757 | [[package]] 758 | name = "stable_deref_trait" 759 | version = "1.2.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 762 | 763 | [[package]] 764 | name = "strsim" 765 | version = "0.11.1" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 768 | 769 | [[package]] 770 | name = "subtle" 771 | version = "2.6.1" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 774 | 775 | [[package]] 776 | name = "syn" 777 | version = "2.0.100" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "unicode-ident", 784 | ] 785 | 786 | [[package]] 787 | name = "synstructure" 788 | version = "0.13.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 791 | dependencies = [ 792 | "proc-macro2", 793 | "quote", 794 | "syn", 795 | ] 796 | 797 | [[package]] 798 | name = "szdt" 799 | version = "0.0.1" 800 | dependencies = [ 801 | "bs58", 802 | "cid", 803 | "clap", 804 | "data-encoding", 805 | "ed25519-dalek", 806 | "multihash", 807 | "rand", 808 | "serde", 809 | "serde_ipld_dagcbor", 810 | "sha2", 811 | "tempfile", 812 | "thiserror", 813 | "unsigned-varint", 814 | "url", 815 | ] 816 | 817 | [[package]] 818 | name = "tempfile" 819 | version = "3.19.1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 822 | dependencies = [ 823 | "fastrand", 824 | "getrandom 0.3.2", 825 | "once_cell", 826 | "rustix", 827 | "windows-sys", 828 | ] 829 | 830 | [[package]] 831 | name = "thiserror" 832 | version = "2.0.12" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 835 | dependencies = [ 836 | "thiserror-impl", 837 | ] 838 | 839 | [[package]] 840 | name = "thiserror-impl" 841 | version = "2.0.12" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 844 | dependencies = [ 845 | "proc-macro2", 846 | "quote", 847 | "syn", 848 | ] 849 | 850 | [[package]] 851 | name = "tinystr" 852 | version = "0.8.1" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 855 | dependencies = [ 856 | "displaydoc", 857 | "zerovec", 858 | ] 859 | 860 | [[package]] 861 | name = "tinyvec" 862 | version = "1.9.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 865 | dependencies = [ 866 | "tinyvec_macros", 867 | ] 868 | 869 | [[package]] 870 | name = "tinyvec_macros" 871 | version = "0.1.1" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 874 | 875 | [[package]] 876 | name = "typenum" 877 | version = "1.18.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 880 | 881 | [[package]] 882 | name = "unicode-ident" 883 | version = "1.0.18" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 886 | 887 | [[package]] 888 | name = "unsigned-varint" 889 | version = "0.8.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 892 | 893 | [[package]] 894 | name = "url" 895 | version = "2.5.4" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 898 | dependencies = [ 899 | "form_urlencoded", 900 | "idna", 901 | "percent-encoding", 902 | "serde", 903 | ] 904 | 905 | [[package]] 906 | name = "utf8_iter" 907 | version = "1.0.4" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 910 | 911 | [[package]] 912 | name = "utf8parse" 913 | version = "0.2.2" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 916 | 917 | [[package]] 918 | name = "version_check" 919 | version = "0.9.5" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 922 | 923 | [[package]] 924 | name = "wasi" 925 | version = "0.11.0+wasi-snapshot-preview1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 928 | 929 | [[package]] 930 | name = "wasi" 931 | version = "0.14.2+wasi-0.2.4" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 934 | dependencies = [ 935 | "wit-bindgen-rt", 936 | ] 937 | 938 | [[package]] 939 | name = "windows-sys" 940 | version = "0.59.0" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 943 | dependencies = [ 944 | "windows-targets", 945 | ] 946 | 947 | [[package]] 948 | name = "windows-targets" 949 | version = "0.52.6" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 952 | dependencies = [ 953 | "windows_aarch64_gnullvm", 954 | "windows_aarch64_msvc", 955 | "windows_i686_gnu", 956 | "windows_i686_gnullvm", 957 | "windows_i686_msvc", 958 | "windows_x86_64_gnu", 959 | "windows_x86_64_gnullvm", 960 | "windows_x86_64_msvc", 961 | ] 962 | 963 | [[package]] 964 | name = "windows_aarch64_gnullvm" 965 | version = "0.52.6" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 968 | 969 | [[package]] 970 | name = "windows_aarch64_msvc" 971 | version = "0.52.6" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 974 | 975 | [[package]] 976 | name = "windows_i686_gnu" 977 | version = "0.52.6" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 980 | 981 | [[package]] 982 | name = "windows_i686_gnullvm" 983 | version = "0.52.6" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 986 | 987 | [[package]] 988 | name = "windows_i686_msvc" 989 | version = "0.52.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 992 | 993 | [[package]] 994 | name = "windows_x86_64_gnu" 995 | version = "0.52.6" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 998 | 999 | [[package]] 1000 | name = "windows_x86_64_gnullvm" 1001 | version = "0.52.6" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1004 | 1005 | [[package]] 1006 | name = "windows_x86_64_msvc" 1007 | version = "0.52.6" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1010 | 1011 | [[package]] 1012 | name = "wit-bindgen-rt" 1013 | version = "0.39.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1016 | dependencies = [ 1017 | "bitflags", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "writeable" 1022 | version = "0.6.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1025 | 1026 | [[package]] 1027 | name = "yoke" 1028 | version = "0.8.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1031 | dependencies = [ 1032 | "serde", 1033 | "stable_deref_trait", 1034 | "yoke-derive", 1035 | "zerofrom", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "yoke-derive" 1040 | version = "0.8.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1043 | dependencies = [ 1044 | "proc-macro2", 1045 | "quote", 1046 | "syn", 1047 | "synstructure", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "zerocopy" 1052 | version = "0.8.25" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 1055 | dependencies = [ 1056 | "zerocopy-derive", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "zerocopy-derive" 1061 | version = "0.8.25" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 1064 | dependencies = [ 1065 | "proc-macro2", 1066 | "quote", 1067 | "syn", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "zerofrom" 1072 | version = "0.1.6" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1075 | dependencies = [ 1076 | "zerofrom-derive", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "zerofrom-derive" 1081 | version = "0.1.6" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1084 | dependencies = [ 1085 | "proc-macro2", 1086 | "quote", 1087 | "syn", 1088 | "synstructure", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "zeroize" 1093 | version = "1.8.1" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1096 | 1097 | [[package]] 1098 | name = "zerotrie" 1099 | version = "0.2.2" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1102 | dependencies = [ 1103 | "displaydoc", 1104 | "yoke", 1105 | "zerofrom", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "zerovec" 1110 | version = "0.11.2" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 1113 | dependencies = [ 1114 | "yoke", 1115 | "zerofrom", 1116 | "zerovec-derive", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "zerovec-derive" 1121 | version = "0.11.1" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1124 | dependencies = [ 1125 | "proc-macro2", 1126 | "quote", 1127 | "syn", 1128 | ] 1129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "szdt" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "szdt" 8 | path = "src/bin/szdt.rs" 9 | 10 | [dependencies] 11 | bs58 = "0.5.1" 12 | cid = { version = "0.11.1", features = ["serde"] } 13 | clap = { version = "4.5.31", features = ["derive"] } 14 | data-encoding = "2.8.0" 15 | ed25519-dalek = { version = "2.1.1", features = [ 16 | "alloc", 17 | "digest", 18 | "rand_core", 19 | "serde", 20 | "signature", 21 | ] } 22 | multihash = "0.19.3" 23 | rand = "0.8.5" 24 | serde = { version = "1.0.219", features = ["derive"] } 25 | serde_ipld_dagcbor = "=0.6.3" 26 | sha2 = "0.10.8" 27 | thiserror = "2.0.12" 28 | unsigned-varint = "0.8.0" 29 | url = { version = "2.5.4", features = ["serde"] } 30 | 31 | [dev-dependencies] 32 | tempfile = "3.19.1" 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gordon Brander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SZDT 2 | 3 | **S**igned **Z**ero-trust **D**a**T**a 4 | 5 | Signed .car files for censorship-resistant publishing and archiving. Pronounced "Samizdat". 6 | 7 | ## Motivation 8 | 9 | Web resources are accessed by URLs (Uniform Resource Locators), meaning they belong to a single canonical location, or "origin". Security on the web is also [dependent upon origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy), making [mirroring](https://en.wikipedia.org/wiki/Mirror_site) difficult. 10 | 11 | All of this means web content is effectively centralized. Centralization makes web data vulnerable to lock-in, censorship, and more banal forms of failure, like [link rot](https://en.wikipedia.org/wiki/Link_rot). A website is a [Single Point of Failure](https://en.wikipedia.org/wiki/Single_point_of_failure) (SPOF), and single points of failure fail eventually. The only question is when. 12 | 13 | These limitations may not be a problem for some kinds of content (private messaging, corporate apps), but they become pressing in the case of archival information and publishing. For example, scientific datasets, journalism, reference information, libraries, academic journals, etc are often intendend to be broadly accessible public goods, available in perpetuity. However, the websites hosting them can and do disappear, as in the 2025 case of [CDC datasets being taken down by the US government](https://www.theatlantic.com/health/archive/2025/01/cdc-dei-scientific-data/681531/). They may also be subject to censorship in many contexts. 14 | 15 | This is a silly situation to be in. [The internet was designed to be decentralized](https://newsletter.squishy.computer/p/decentralization-enables-permissionless)—decentralized enough to survive a nuclear war. Yet the web and internet have centralized through an [unfortunate combo of technical choices and commercial pressures](https://newsletter.squishy.computer/i/65395829/redecentralizing-the-web). We can't fix all of that, but we can make it easier to distribute content to multiple redundant locations. Let's de-SPOF data. 16 | 17 | To maintain a resilient information ecosystem, we need a simple way to publish and archive information that: 18 | 19 | - Is decentralized, redundant, and censorship-resistant 20 | - Keeps long-tail content alive over long periods of time 21 | - Is easy to adopt **right now**, with infrastructure that is already widely deployed. 22 | 23 | ## The idea 24 | 25 | **TLDR**: a cryptographically-signed .car file containing: 26 | 27 | - **Files** stored as raw bytes 28 | - **Links** to additional external files, with content addresses for verifying integrity and multiple redundant URLs for retrieval 29 | - **Address book**, mapping known public keys to [petnames](https://files.spritely.institute/papers/petnames.html). 30 | - **Cryptographic claims** proving the authenticity of the archive 31 | 32 | ## Goals 33 | 34 | - **Zero-trust**: SZDT archives are verified using cryptographic hashing and public key cryptography. No centralized authorities are required. 35 | - **Decentralized**: [Lots Of Copies Keeps Stuff Safe](https://www.lockss.org/). SZDT archives are made to be distributed to many redundant locations, including multiple HTTP servers, BitTorrent, hard drives, etc. Likewise, URLs in SZDT files point to many redundant locations, including HTTP servers, BitTorrent, and more. 36 | - **Censorship-resistant**: Distributable via HTTP, Torrents, email, airdrop, sneakernet, or anything else. 37 | - **Anonymous/pseudonymous**: SZDT uses [keys, not IDs](https://newsletter.squishy.computer/i/60168330/keys-not-ids-toward-personal-illegibility). No accounts are required. This allows for anonymous and pseudonymous publishing. If an author wishes to be publicly known, they can use other protocols to link a key to their identity. 38 | - **Discoverable**: SZDT archives contain multiple pointers to places where other archives and keys can be found. With just one or two SZDT files, you can follow the links to construct your own web of trust and collection of archives. 39 | - **Boring**: SZDT uses ubiquitous technology. It is designed to be compatible with widely deployed infrastructure, like HTTP. The format is simple, and easy to implement in any language. 40 | 41 | If there are many copies, and many ways to find them, then data can survive the way dandelions do—by spreading seeds. 42 | 43 | ### Non-goals 44 | 45 | - **P2P**: SZDT is transport-agnostic. It's just a file format. You should be able to publish, share, and retreive SZDT archives from anywhere, including HTTP, BitTorrent, email, messaging apps, sneakernet, etc. 46 | - **Efficiency**: SZDT is not efficient. Its goal is to be simple and resilient, like a cockroach. We don't worry about efficient chunking, or deduping. When efficient downloading is needed, we leverage established protocols like BitTorrent. 47 | - **Comprehensive preservation**: SZDT doesn't aim for comprehensive preservation. Instead it aims to make it easy to spread data like dandelion seeds. Dandelions are difficult to weed out. 48 | 49 | ## Specification 50 | 51 | See [spec.md](./spec.md). 52 | 53 | ## Development 54 | 55 | ### Installing binaries on your path with Cargo 56 | 57 | From the project directory: 58 | 59 | ```bash 60 | cargo install --path . 61 | ``` 62 | 63 | This will install the binaries to `~/.cargo/bin`, which is usually added to your path by the Rust installer. 64 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | # SZDT 2 | 3 | **S**igned **Z**ero-trust **D**a**T**a 4 | 5 | Signed .car files for censorship-resistant publishing and archiving. Pronounced "Samizdat". 6 | 7 | ## Motivation 8 | 9 | Web resources are accessed by URLs (Uniform Resource Locators), meaning they belong to a single canonical location, or "origin". Security on the web is also [dependent upon origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy), making [mirroring](https://en.wikipedia.org/wiki/Mirror_site) difficult. 10 | 11 | All of this means web content is effectively centralized. Centralization makes web data vulnerable to lock-in, censorship, and more banal forms of failure, like [link rot](https://en.wikipedia.org/wiki/Link_rot). A website is a [Single Point of Failure](https://en.wikipedia.org/wiki/Single_point_of_failure) (SPOF), and single points of failure fail eventually. The only question is when. 12 | 13 | These limitations may not be a problem for some kinds of content (private messaging, corporate apps), but they become pressing in the case of archival information and publishing. For example, scientific datasets, journalism, reference information, libraries, academic journals, etc are often intendend to be broadly accessible public goods, available in perpetuity. However, the websites hosting them can and do disappear, as in the 2025 case of [CDC datasets being taken down by the US government](https://www.theatlantic.com/health/archive/2025/01/cdc-dei-scientific-data/681531/). They may also be subject to censorship in many contexts. 14 | 15 | This is a silly situation to be in. [The internet was designed to be decentralized](https://newsletter.squishy.computer/p/decentralization-enables-permissionless)—decentralized enough to survive a nuclear war. Yet the web and internet have centralized through an [unfortunate combo of technical choices and commercial pressures](https://newsletter.squishy.computer/i/65395829/redecentralizing-the-web). We can't fix all of that, but we can make it easier to distribute content to multiple redundant locations. Let's de-SPOF data. 16 | 17 | To maintain a resilient information ecosystem, we need a simple way to publish and archive information that: 18 | 19 | - Is decentralized, redundant, and censorship-resistant 20 | - Keeps long-tail content alive over long periods of time 21 | - Is easy to adopt **right now**, with infrastructure that is already widely deployed. 22 | 23 | ## The idea 24 | 25 | **TLDR**: a cryptographically-signed .car file containing: 26 | 27 | - **Files** stored as raw bytes 28 | - **Links** to additional external files, with content addresses for verifying integrity and multiple redundant URLs for retrieval 29 | - **Address book**, mapping known public keys to [petnames](https://files.spritely.institute/papers/petnames.html). 30 | - **Cryptographic claims** proving the authenticity of the archive 31 | 32 | ## Goals 33 | 34 | - **Zero-trust**: SZDT archives are verified using cryptographic hashing and public key cryptography. No centralized authorities are required. 35 | - **Decentralized**: [Lots Of Copies Keeps Stuff Safe](https://www.lockss.org/). SZDT archives are made to be distributed to many redundant locations, including multiple HTTP servers, BitTorrent, hard drives, etc. Likewise, URLs in SZDT files point to many redundant locations, including HTTP servers, BitTorrent, and more. 36 | - **Censorship-resistant**: Distributable via HTTP, Torrents, email, airdrop, sneakernet, or anything else. 37 | - **Anonymous/pseudonymous**: SZDT uses [keys, not IDs](https://newsletter.squishy.computer/i/60168330/keys-not-ids-toward-personal-illegibility). No accounts are required. This allows for anonymous and pseudonymous publishing. If an author wishes to be publicly known, they can use other protocols to link a key to their identity. 38 | - **Discoverable**: SZDT archives contain multiple pointers to places where other archives and keys can be found. With just one or two SZDT files, you can follow the links to construct your own web of trust and collection of archives. 39 | - **Boring**: SZDT uses ubiquitous technology. It is designed to be compatible with widely deployed infrastructure, like HTTP. The format is simple, and easy to implement in any language. 40 | 41 | If there are many copies, and many ways to find them, then data can survive the way dandelions do—by spreading seeds. 42 | 43 | ### Non-goals 44 | 45 | - **P2P**: SZDT is transport-agnostic. It's just a file format. You should be able to publish, share, and retreive SZDT archives from anywhere, including HTTP, BitTorrent, email, messaging apps, sneakernet, etc. 46 | - **Efficiency**: SZDT is not efficient. Its goal is to be simple and resilient, like a cockroach. We don't worry about efficient chunking, or deduping. When efficient downloading is needed, we leverage established protocols like BitTorrent. 47 | - **Comprehensive preservation**: SZDT doesn't aim for comprehensive preservation. Instead it aims to make it easy to spread data like dandelion seeds. Dandelions are difficult to weed out. 48 | 49 | # Speculative specification 50 | 51 | ## High‑level goals 52 | 53 | | Requirement | Design choice | 54 | |-------------|---------------| 55 | | _Self‑verifying per‑object_ | Every blob is stored as a **raw IPLD block** whose CID’s multihash is the SHA‑256 of the exact bytes in the block. | 56 | | _Self‑verifying per‑archive_ | The archive header contains one or more **JWT claims** whose payload commits to the set a CID in the archive and is signed by a public key that can be resolved independently (e.g. DID). | 57 | | _Streaming friendliness_ | Sticks to CAR v1’s _length‑prefixed block stream_ so writers can pipe without seeking. | 58 | | _Forward compatibility_ | Extra header field `claims` is added; CAR v1 readers that only read `roots`+`version` will simply ignore it. | 59 | 60 | ## File layout 61 | 62 | SZDT is built on top of [DASL CAR v1](https://dasl.ing/car.html), a simple file format that contains CBOR headers, followed by blocks of content-addressed data. 63 | 64 | ``` 65 | |------- Header -------| |------------------- Data -------------------| 66 | [ int | DAG-CBOR block ] [ int | CID | block ] [ int | CID | block ] … 67 | ``` 68 | 69 | Because every block is content-addressed, CAR files contain everything you need to verify the integrity of the archive. CAR is used by Bluesky's [ATProtocol](https://atproto.com/specs/repository), and the IPFS ecosystem, making it a good starting point. 70 | 71 | ### Block order 72 | 73 | While CAR does not mandate a block order, SZDT CAR writers SHOULD write blocks in first-seen order, as a result of a depth-first DAG traversal starting from root(s), in order of roots. This ensures efficient streaming when reading. Practically speaking, assuming the archive manifest is the only root, this should mean that the archive manifest block should be written first. 74 | 75 | > Note: [Filecoin implementation uses this same block order](https://ipld.io/specs/transport/car/carv1/#determinism), and also restricts roots to a single CID. 76 | 77 | In keeping with the [robustness principle](https://en.wikipedia.org/wiki/Robustness_principle), SZDT CAR readers MUST NOT assume block order, and MUST accept blocks in any order. 78 | 79 | ### Content Type 80 | 81 | CAR files have a mime type of `application/vnd.ipld.car`. 82 | 83 | ## Header schema 84 | 85 | An SZDT CAR header has the following structure: 86 | 87 | ```cbor 88 | { 89 | "version": 1, 90 | "roots": [ , … ], 91 | "claims": [ , … ] ; new, optional 92 | } 93 | ``` 94 | 95 | * `roots` MUST contain the dag-cbor CID for the archive manifest. 96 | * `claims` contains array of zero or more JWT claims signing over a CID, which is typically the CID for the archive manifest. 97 | 98 | ## Content Identifiers (CIDs) 99 | 100 | Content proofs are described with Content Identifiers (CIDs). CIDs are essentially file hashes with some additional bytes for metadata. A CID's structure is: 101 | 102 | ``` 103 | 104 | ``` 105 | 106 | ...where multibase, version, multicodec, multihash, and length are [LEB128](https://en.wikipedia.org/wiki/LEB128) integers, and digest is the bytes of the hash digest. 107 | 108 | SZDT supports two kinds of CID, both specified in [DASL CID](https://dasl.ing/cid.html). 109 | 110 | **Raw CID**: CID v1 with raw codec (0x55) and SHA-256 (0x12) hash: 111 | 112 | ``` 113 | # Raw CID prefix 114 | 0x01 0x55 0x12 0x20 ... 115 | ``` 116 | 117 | **dag-cbor CID**: CID v1 with dag-cbor codec (0x71) and SHA-256 (0x12) hash: 118 | 119 | ``` 120 | # dag-cbor CID prefix 121 | 0x01 0x71 0x12 0x20 ... 122 | ``` 123 | 124 | CIDs may be [multibase base-encoded](https://github.com/multiformats/multicodec/blob/master/table.csv) as string or bytes. When encoded as a string, only lowercase base32 is supported. 125 | 126 | ## Archive manifest 127 | 128 | The archive manifest is a dag-cbor map containing a map of files and contacts. 129 | 130 | ```typescript 131 | type ArchiveManifest = { 132 | // Display name for archive 133 | dn: String; 134 | // Map of file paths to links 135 | files: Record; 136 | // Map of DIDs to petnames 137 | contacts: Record; 138 | } 139 | 140 | type Link = { 141 | // CID for file 142 | cid: CID; 143 | // Zero or more URLs where it may be found 144 | location: Url[]; 145 | } 146 | ``` 147 | 148 | ## Claims 149 | 150 | - **Serialization**: JWS Compact (`..`) 151 | - **Algorithm**: Ed25519 (`EdDSA`). 152 | - **Header** MUST include `kid` with `did:key` so verifiers can obtain the public key. 153 | 154 | ### Payload claims (registered + private) 155 | 156 | | Claim | Type | Description | 157 | |-------|------|-------------| 158 | | `iss` | URI | Issuer (e.g. `did:key:…`). | 159 | | `iat` | Int | Issued‑at (seconds since epoch). | 160 | | `exp` | Int? | (Optional) Expiry. | 161 | | `cid` | String\[] | lowercase base32 CID string being vouched for. | 162 | | `kind` | `"witness"` | Explicit type tag for extensibility. | 163 | 164 | > **Signing input**: the canonical JSON payload bytes (UTF‑8, no whitespace) are signed. Verifiers MUST canonicalise before hashing. 165 | 166 | ### Verification procedure 167 | 168 | **Integrity** of SZDT archives may be verified using standard CAR reading procedures: 169 | 170 | - For each block in the CAR file 171 | - Retrieve the CID for the block 172 | - Recompute the CID for the block, using the same codec as the block cid 173 | - Compare CIDs to verify cryptographic integrity 174 | - If every block CID matches the equivalent recomputed CID, the CAR's integrity is considered valid. 175 | 176 | **Authenticity** of the archive may be verified along multiple dimensions by verifying the JWTs in the `claims` header according to the verification procedures outlined in [RFC7519](https://datatracker.ietf.org/doc/html/rfc7519). 177 | 178 | In particular, implementors may wish to check for the presence of a `witness` claim, signed by a key trusted by the user. Verifying the claim provides a cryptographic proof that the key witnessed a cid. When an archive manifest is witnessed, users can be assured that the entire archive contents are witnessed, since content referenced by the manifest is content addressed. 179 | 180 | If the **integrity** of the CAR is verfied, and the **authenticity** of the CAR has been verified for any claims relevant to the use-case, then the archive is considered valid. 181 | 182 | Note that headers are neither signed nor verified for integrity, by design. All proofs are made over the blocks of the CAR file, with the CAR headers acting as modifiable metadata. This supports workflows where multible actors may witness or amend to a CAR file without invalidating the proofs and claims made by other actors over subsections of the archive. Nevertheless, when retreiving a CAR via content addressing, integrity of the entire file, including headers, may be verified. 183 | 184 | ## Example (illustrative) 185 | 186 | ```jsonc 187 | // Header (object before CBOR encoding) 188 | { 189 | "version": 1, 190 | "roots": [ 191 | "bafkreigh2akiscaildcf…" // manifest 192 | ], 193 | "claims": [ 194 | "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZN..." // compact JWT 195 | ] 196 | } 197 | ``` 198 | 199 | # Appendix 200 | 201 | 202 | ## Extensibility & interop notes 203 | 204 | - **Unknown header keys**: The CAR v1 spec states that only `roots` and `version` are required; decoders that ignore unknown keys remain compliant. 205 | - **Manifest**: To avoid gigantic root arrays, store one DAG‑CBOR manifest node listing all blob CIDs and use *that* CID as the single root. CAR does not mandate single root, but it seems to be the norm. This ensures that everything in the DAG-CBOR structure is verified for integrity, and also gives us a single CID to sign over. 206 | 207 | ## References 208 | 209 | ## Example use-cases 210 | 211 | Here are a few things you can do, or that should be easy to do with SZDT. 212 | 213 | - Publishing and downloading via HTTP. 214 | - Downloading unpacked files (or zipped archive) from the browser. 215 | - Accomplishable through a JS library or browser extension. 216 | - Sharing via [sneakernet](https://en.wikipedia.org/wiki/Sneakernet) (USB, Airdrop, SD cards, etc). Sneakernet methods become important when the internet is censored or shut down. 217 | - During the 2019-2020 Hong Kong protests, protestors used Airdrop to route around censorship: "AirDrop was used to broadcast anti-extradition bill information to the public and mainland tourists" ([Wikipedia](https://en.wikipedia.org/wiki/2019%E2%80%932020_Hong_Kong_protests#Moderate_group)). 218 | - SD cards are small and high-capacity. Most Android phones can load data, as well as side-load apps directly from an SD card placed in the expansion slot. 219 | - Cold storage on hard drives 220 | - Private data backups 221 | - Long-term archival 222 | 223 | ## FAQ 224 | 225 | - Why not just use torrents? 226 | - We do use Torrents! ...and HTTP, and anything else that can move bits. 227 | - Why support HTTP? Why not just a Torrent Tracker/IPFS/etc? 228 | - Setting up a BitTorrent or IPFS server isn't always easy. Many ISPs block or throttle common p2p ports, and outbound bandwidth costs can be hard to manage. 229 | - BitTorrent and other p2p protocols often have trouble keeping long-tail archival content alive. 230 | - The world is optimized around serving files over HTTP. It's easy, it's everywhere, and we should leverage it for redundancy and availability, while additionally leveraging Torrents when needed. 231 | - Why embed files? Why not just use links? 232 | - Nothing is more decentralized than having the actual bytes. Making it possible to embed file bytes supports redundancy, resiliency, censorship resistance. 233 | - Why links? Why not just embed files? 234 | - Some archives are too big to fit into one file. Links allow authors to externalize resources that might be difficult or expensive to embed. 235 | - Having both links and files allows authors to "turn the dial" on where the bytes live. For example, you might embed text files, but link out to LOTR_4K_ExtendedEdition.mp4. 236 | - Why not Internet Archive/Library of Congress/LibGen/Anna's Archive/Sci-Hub? 237 | - SZDT could be used with or alongside any of these. Our goal is not to replace archival projects, but to offer a (possibly complimentary) "dandelion seed" file format. 238 | - Like our namesake, the focus of SZDT is on censorship-resistant self-publishing. Our use-cases include archiving, but also smaller-scale tasks such as personal archives, notes, and other content that may never make it into these larger archives. 239 | - Why CAR? 240 | - Existing specs 241 | - [DASL](https://dasl.ing/). 242 | - Simple 243 | - Like TAR, it is basically a sequence of blocks. 244 | - Even if everyone forgets what CAR is in 100 years, you could poke at the bytes and write a decoder in a day. 245 | - Open-ended metadata 246 | - CAR supports CBOR headers and DAG-CBOR blocks. 247 | - Integrity verification 248 | - Each block is prefixed by CIDs. 249 | - Streaming 250 | - Easy to append new blocks to the end. 251 | - Archive files can be quite large, and streaming processing may be a necessity for some collections. 252 | - This is why many archival projects use WARC or TAR — formats that are essentially flat sequences of blocks. 253 | - Can be easily unpacked in a browser context 254 | - [ATProto](https://atproto.com/specs/repository#car-file-serialization) supports CAR. A small ecosystem of tooling is being built around CAR because AtProto PDS supports CAR. 255 | - E.g. [satnav])(https://github.com/blacksky-algorithms/rsky/tree/main/rsky-satnav), 256 | - CAR does have disadvantages. 257 | - It's binary, not human readable. Higher barrier to entry vs JSON. 258 | - It's not an IETF standard 259 | - Fewer implementations 260 | - You also need a CBOR (ideally a dag-cbor) implementation 261 | - CIDs (particularly the LEB128 ints) are more complicated than just a hash 262 | - However, the pros outweigh the cons for this project. 263 | - Why not... 264 | - CBOR 265 | - Advantages 266 | - IETF standard 267 | - Streaming parsing 268 | - Widely available libraries 269 | - Disadvantages 270 | - It's binary, not human readable. Higher barrier to entry vs JSON. 271 | - Higher barrier to entry vs JSON 272 | - Requirement to close body makes streaming-appending workflows difficult 273 | - JSON 274 | - Advantages 275 | - Ubiquitous 276 | - Human-readable and authorable 277 | - Disadvantages 278 | - Can't embed file bytes without base encoding 279 | - Zip 280 | - Advantages 281 | - Ubiquitous 282 | - Users can un-archive using built-in OS tools 283 | - Random access 284 | - Disadvantages 285 | - No streaming parsing 286 | - Can't append blocks 287 | - Higher barrier to entry in browser environment 288 | - Metadata is more difficult (easiest approach is to add a manifest file to ZIP) 289 | - TAR 290 | - Advantages 291 | - Ubiquitous 292 | - Users can un-archive using built-in OS tools (except on Windows) 293 | - Streaming 294 | - Archival projects already use it 295 | - Disadvantages 296 | - Metadata is more difficult (easiest approach is to add a manifest file) 297 | - Why censorship-resistance? 298 | - Censorship-restance is another way of saying "resilience". There are many reasons to want resilient, decentralized knowledge repositories. 299 | 300 | ## Design principles 301 | 302 | - Seeding content should be as easy as uploading a file to an ordinary HTTP server. 303 | - Runs in the browser. If it doesn't, you're DOA. Ideally it "just works", but given the pervasiveness of cross-origin restrictions, we'll settle for a JavaScript polyfill. 304 | - Who and what, not where and how. SZDT verifies authenticity (who) with public keys, and file integrity (what) with hashes. We don't care where the file lives (which server) or how you got it (which transport). This enhances censorship resistance, since the data can be shared through any mechanism. 305 | - Think in files, because the world thinks in files. We don't worry about content chunking, like BitTorrent, or content-addressed DAGs, like IPFS. Files are good enough. They aren't perfect, but they are simple and ubiquitous. 306 | - Be fault-tolerant. Embrace the fact that we might not be able to retrieve every part of an archive. Some files might disappear. It's better to get some than none. 307 | 308 | ## Acknowledgements 309 | 310 | SZDT begs, borrows and steals inspiration from a number of smart people and projects: 311 | 312 | - [Noosphere](https://github.com/subconsciousnetwork/noosphere): for the idea of combining archives with p2p discovery via address books and petnames. 313 | - [Nostr](https://nostr.com/protocol): for the emphasis on extreme simplicity, self-sovereign signing, and the use of boring HTTP relays for censorship-resistant publishing. 314 | - WebPackaging / WebBundles proposal for sketching out a way to bundle web resources and distribute them across multiple origins. 315 | - BitTorrent: for being a p2p protocol that actually works. 316 | - Magnet links: for the idea of describing redundant locations. 317 | - [Iroh](https://github.com/n0-computer/iroh), for showing that the ideal chunk size is large, not small. This obliquely inspired us to say that files are good enough. They're a chunk that is already compatible with existing infrastructure. 318 | - RSS, for being super easy to adopt using existing infrastructure. 319 | 320 | Other related projects and prior art: 321 | 322 | - Hashlink 323 | - did:key 324 | - Metalink: offering a similar idea, and for helping define some some non-goals, such as content-chunking. We can fall back to BitTorrent for effecient chunked downloads. Our design goal is resilience, not efficiency. 325 | -------------------------------------------------------------------------------- /src/base58btc.rs: -------------------------------------------------------------------------------- 1 | use bs58; 2 | 3 | /// Encode bytes using Base58BTC encoding. 4 | pub fn encode(bytes: I) -> String 5 | where 6 | I: AsRef<[u8]>, 7 | { 8 | bs58::encode(bytes).into_string() 9 | } 10 | 11 | pub type DecodeError = bs58::decode::Error; 12 | 13 | /// Decode bytes from Base58BTC encoding. 14 | pub fn decode(s: &str) -> Result, DecodeError> { 15 | let bytes = bs58::decode(s).into_vec()?; 16 | Ok(bytes) 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/szdt.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use std::ffi::OsStr; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::path::PathBuf; 6 | use szdt::base58btc; 7 | use szdt::car::{CarBlock, CarReader, CarWriter}; 8 | use szdt::car_claim_header::CarClaimHeader; 9 | use szdt::claim::{self, Assertion, Claim, WitnessAssertion}; 10 | use szdt::ed25519::generate_keypair; 11 | use szdt::file::walk_files; 12 | use szdt::manifest::Manifest; 13 | 14 | #[derive(Parser)] 15 | #[command(version = "0.0.1")] 16 | #[command(author = "szdt")] 17 | #[command(about = "Censorship-resistant publishing and archiving")] 18 | struct Cli { 19 | #[command(subcommand)] 20 | command: Commands, 21 | } 22 | 23 | #[derive(Subcommand)] 24 | enum Commands { 25 | #[command(about = "Unpack a .car archive")] 26 | Unarchive { 27 | #[arg(help = "Archive file")] 28 | #[arg(value_name = "FILE")] 29 | file: PathBuf, 30 | #[arg( 31 | value_name = "DIR", 32 | short, 33 | long, 34 | help = "Directory to unpack archive into. Defaults to archive file name." 35 | )] 36 | dir: Option, 37 | }, 38 | 39 | #[command(about = "Create a .car archive from a folder full of files")] 40 | Archive { 41 | #[arg(help = "Directory to archive")] 42 | #[arg(value_name = "DIR")] 43 | dir: PathBuf, 44 | 45 | #[arg(help = "Private key to sign archive with")] 46 | #[arg( 47 | long_help = "Private key to sign archive with. The private key should be a Base-32 encoded Ed25519 key. You can generate a key using the `genkey` command.)" 48 | )] 49 | #[arg(short, long)] 50 | #[arg(value_name = "KEY")] 51 | privkey: Option, 52 | }, 53 | 54 | #[command(about = "Generate a private key")] 55 | Genkey {}, 56 | } 57 | 58 | fn archive(dir: PathBuf, secret_key: Option) { 59 | let default_file_name = OsStr::new("archive"); 60 | 61 | let file_name = 62 | PathBuf::from(dir.file_stem().unwrap_or(default_file_name)).with_extension("car"); 63 | 64 | println!("Writing archive: {}", file_name.display()); 65 | 66 | let manifest = Manifest::from_dir(&dir).expect("Unable to create manifest"); 67 | 68 | // Create a new CarBlock for the manifest. 69 | // We'll sign over its CID. 70 | let manifest_block = 71 | CarBlock::from_serializable(&manifest).expect("Unable to generate CID from manifest"); 72 | 73 | println!("manifest -> {}", manifest_block.cid()); 74 | 75 | let claims = match secret_key { 76 | Some(secret_key) => { 77 | let secret_key_bytes = 78 | base58btc::decode(&secret_key).expect("Secret key base encoding is invalid"); 79 | 80 | let witness_claim = claim::Builder::new(&secret_key_bytes) 81 | .expect("Unable to build claim") 82 | .add_ast(Assertion::Witness(WitnessAssertion { 83 | cid: manifest_block.cid().clone(), 84 | })) 85 | .sign() 86 | .expect("Unable to sign claim"); 87 | 88 | vec![witness_claim] 89 | } 90 | None => vec![], 91 | }; 92 | 93 | let header = CarClaimHeader::new(vec![manifest_block.cid().clone()], claims); 94 | 95 | let car_file = fs::File::create(&file_name).expect("Failed to create archive file"); 96 | let mut car = CarWriter::new(car_file, &header).expect("Should be able to create car"); 97 | 98 | // Write manifest block first 99 | manifest_block 100 | .write_into(&mut car) 101 | .expect("Unable to write manifest to CAR"); 102 | 103 | for path in walk_files(&dir).expect("Directory should be readable") { 104 | let body = fs::read(&path).expect("Path should be readable"); 105 | let block = CarBlock::from_raw(body); 106 | block 107 | .write_into(&mut car) 108 | .expect("Should be able to write block to car file"); 109 | println!("{} -> {}", &path.display(), block.cid()); 110 | } 111 | 112 | car.flush() 113 | .expect("Should be able to flush all writes to car file"); 114 | 115 | println!("Archive created: {}", file_name.display()); 116 | } 117 | 118 | fn unarchive(file_path: PathBuf, dir: Option) { 119 | let file = fs::File::open(&file_path).expect("Should be able to open file"); 120 | 121 | let reader: CarReader<_, CarClaimHeader> = 122 | CarReader::read_from(file).expect("Should be able to read car file"); 123 | 124 | let header = reader.header(); 125 | 126 | let validated_claims: Vec = header 127 | .claims 128 | .iter() 129 | .filter(|claim| { 130 | let result = claim.validate(None); 131 | if let Err(err) = &result { 132 | eprintln!("Claim error: {}", err); 133 | } 134 | result.is_ok() 135 | }) 136 | .map(|claim| claim.clone()) 137 | .collect(); 138 | 139 | println!( 140 | "Validated {} of {} claims", 141 | validated_claims.len(), 142 | header.claims.len() 143 | ); 144 | for claim in validated_claims { 145 | println!("{}", claim.payload().iss); 146 | } 147 | 148 | // Create a folder named after the file path 149 | let archive_dir = match dir { 150 | Some(dir) => dir, 151 | None => file_path 152 | .file_stem() 153 | .map(|p| p.into()) 154 | .unwrap_or("archive".into()), 155 | }; 156 | 157 | fs::create_dir(&archive_dir).expect("Should be able to create directory"); 158 | 159 | for block in reader { 160 | let block = block.expect("Should be able to read block"); 161 | let path = archive_dir.join(block.cid().to_string()); 162 | let body = block.body(); 163 | fs::write(&path, body).expect("Should be able to write file"); 164 | println!("{} -> {}", block.cid(), &path.display()); 165 | } 166 | println!("Unpacked archive"); 167 | } 168 | 169 | fn genkey() { 170 | let (_, privkey) = generate_keypair(); 171 | let encoded_key = base58btc::encode(privkey); 172 | println!("{}", encoded_key); 173 | } 174 | 175 | fn main() { 176 | let cli = Cli::parse(); 177 | match cli.command { 178 | Commands::Archive { dir, privkey } => archive(dir, privkey), 179 | Commands::Unarchive { file, dir } => unarchive(file, dir), 180 | Commands::Genkey {} => genkey(), 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/byte_counter_reader.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | 3 | /// A wrapper around a reader that counts the number of bytes read. 4 | pub(crate) struct ByteCounterReader { 5 | reader: R, 6 | /// The number of bytes that have been read so far. 7 | read_size: usize, 8 | } 9 | 10 | impl ByteCounterReader { 11 | /// Creates a new `ByteCountingReader` wrapping the given reader. 12 | pub(crate) fn new(reader: R) -> Self { 13 | ByteCounterReader { 14 | reader, 15 | read_size: 0, 16 | } 17 | } 18 | 19 | /// Returns the number of bytes that have been read so far. 20 | pub(crate) fn read_size(&self) -> usize { 21 | self.read_size 22 | } 23 | } 24 | 25 | impl Read for ByteCounterReader { 26 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 27 | let bytes = self.reader.read(buf)?; 28 | self.read_size += bytes; 29 | Ok(bytes) 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use std::io::{BufReader, Cursor}; 37 | 38 | #[test] 39 | fn test_byte_counter_reader_counts_the_bytes() { 40 | let data = b"hello world"; 41 | let cursor = Cursor::new(data); 42 | let mut reader = ByteCounterReader::new(cursor); 43 | 44 | // Read a few bytes 45 | let mut buf = [0u8; 5]; 46 | let bytes_read = reader.read(&mut buf).unwrap(); 47 | assert_eq!(bytes_read, 5); 48 | assert_eq!(&buf, b"hello"); 49 | assert_eq!(reader.read_size(), 5); 50 | 51 | // Read the rest 52 | let mut buf = [0u8; 10]; 53 | let bytes_read = reader.read(&mut buf).unwrap(); 54 | assert_eq!(bytes_read, 6); 55 | assert_eq!(&buf[..bytes_read], b" world"); 56 | assert_eq!(reader.read_size(), 11); 57 | 58 | // Try to read more (should return 0 bytes read) 59 | let mut buf = [0u8; 5]; 60 | let bytes_read = reader.read(&mut buf).unwrap(); 61 | assert_eq!(bytes_read, 0); 62 | assert_eq!(reader.read_size(), 11); 63 | } 64 | 65 | #[test] 66 | fn test_empty_byte_counter_reader_counts_zero() { 67 | let data = b""; 68 | let cursor = Cursor::new(data); 69 | let mut reader = ByteCounterReader::new(cursor); 70 | 71 | let mut buf = [0u8; 5]; 72 | let bytes_read = reader.read(&mut buf).unwrap(); 73 | assert_eq!(bytes_read, 0); 74 | assert_eq!(reader.read_size(), 0); 75 | } 76 | 77 | #[test] 78 | fn test_byte_counter_reader_plays_well_with_bufreader() { 79 | let data = b"hello world"; 80 | let cursor = Cursor::new(data); 81 | let bufcursor = BufReader::new(cursor); 82 | let mut reader = ByteCounterReader::new(bufcursor); 83 | 84 | let mut buf = [0u8; 5]; 85 | let bytes_read = reader.read(&mut buf).unwrap(); 86 | assert_eq!(bytes_read, 5); 87 | assert_eq!(&buf, b"hello"); 88 | assert_eq!(reader.read_size(), 5); 89 | 90 | let mut buf = [0u8; 10]; 91 | let bytes_read = reader.read(&mut buf).unwrap(); 92 | assert_eq!(bytes_read, 6); 93 | assert_eq!(&buf[..bytes_read], b" world"); 94 | assert_eq!(reader.read_size(), 11); 95 | 96 | let mut buf = [0u8; 5]; 97 | let bytes_read = reader.read(&mut buf).unwrap(); 98 | assert_eq!(bytes_read, 0); 99 | assert_eq!(reader.read_size(), 11); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/car.rs: -------------------------------------------------------------------------------- 1 | use crate::byte_counter_reader::ByteCounterReader; 2 | use crate::cid::{read_into_cid_v1_cbor, read_into_cid_v1_raw}; 3 | use crate::multihash::{self, read_into_multihash}; 4 | use crate::varint::{self, read_varint_usize, write_usize_varint}; 5 | use cid::Cid; 6 | use serde::{Deserialize, Serialize, de, de::DeserializeOwned, ser}; 7 | use serde_ipld_dagcbor; 8 | use std::io::{self, Read, Write}; 9 | use thiserror::Error; 10 | 11 | pub struct CarReader { 12 | header: H, 13 | reader: R, 14 | } 15 | 16 | impl CarReader { 17 | pub fn header(&self) -> &H { 18 | &self.header 19 | } 20 | 21 | /// Unwrap, returning the inner reader. 22 | pub fn into_inner(self) -> R { 23 | self.reader 24 | } 25 | } 26 | 27 | impl CarReader { 28 | /// Read bytes into a Car file. 29 | pub fn read_from(mut reader: R) -> Result { 30 | // Get header length 31 | let header_length = read_varint_usize(&mut reader)?; 32 | // Create a `header_length` buffer and read bytes from the header block 33 | let mut header_buffer = vec![0; header_length]; 34 | reader.read_exact(&mut header_buffer)?; 35 | // Deserialize header 36 | let header: H = serde_ipld_dagcbor::from_slice(&header_buffer) 37 | .map_err(|e| Error::Serialization(e.to_string()))?; 38 | return Ok(Self { header, reader }); 39 | } 40 | } 41 | 42 | impl Read for CarReader { 43 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 44 | self.reader.read(buf) 45 | } 46 | } 47 | 48 | impl Iterator for CarReader { 49 | type Item = Result; 50 | 51 | fn next(&mut self) -> Option { 52 | // Try to read the next block 53 | match CarBlock::read_from(&mut self.reader) { 54 | Ok(block) => Some(Ok(block)), 55 | Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => None, 56 | Err(e) => Some(Err(e)), 57 | } 58 | } 59 | } 60 | 61 | pub struct CarWriter { 62 | writer: W, 63 | } 64 | 65 | impl CarWriter { 66 | /// Create a new `CarWriter` instance, writing the CAR header to the writer. 67 | pub fn new(mut writer: W, header: &H) -> Result { 68 | // Serialize header to dag-cbor 69 | let header_cbor = 70 | serde_ipld_dagcbor::to_vec(header).map_err(|e| Error::Serialization(e.to_string()))?; 71 | // Write length 72 | varint::write_usize_varint(&mut writer, header_cbor.len())?; 73 | // Write header 74 | writer.write_all(&header_cbor)?; 75 | Ok(CarWriter { writer }) 76 | } 77 | 78 | /// Unwrap, returning the inner writer. 79 | pub fn into_inner(self) -> W { 80 | self.writer 81 | } 82 | } 83 | 84 | impl Write for CarWriter { 85 | fn write(&mut self, buf: &[u8]) -> io::Result { 86 | self.writer.write(buf) 87 | } 88 | 89 | fn flush(&mut self) -> io::Result<()> { 90 | self.writer.flush() 91 | } 92 | } 93 | 94 | /// The CAR header of an SZDT archive. 95 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 96 | pub struct CarHeader { 97 | version: u64, 98 | pub roots: Vec, 99 | } 100 | 101 | impl CarHeader { 102 | /// Construct a new CarHeader 103 | pub fn new_v1() -> Self { 104 | CarHeader { 105 | version: 1, 106 | roots: Vec::new(), 107 | } 108 | } 109 | } 110 | 111 | /// A single block of data in a CAR file. 112 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 113 | pub struct CarBlock { 114 | cid: Cid, 115 | body: Vec, 116 | } 117 | 118 | impl CarBlock { 119 | /// Construct a new CarBlock 120 | /// This method cryptographically verifies the contents of the block by 121 | /// reconstructing the CID from the body and comparing it to the provided CID. 122 | pub fn new(cid: Cid, body: Vec) -> Result { 123 | let mut reader = body.as_slice(); 124 | let hash = read_into_multihash(&mut reader)?; 125 | let actual_cid = Cid::new_v1(cid.codec(), hash); 126 | if actual_cid != cid { 127 | return Err(Error::InvalidBlock(format!( 128 | "CID doesn't match.\n\tExpected: {}\n\tActual: {}", 129 | cid, actual_cid 130 | ))); 131 | } 132 | Ok(CarBlock { cid, body }) 133 | } 134 | 135 | /// Constructs a new CarBlock from raw data 136 | pub fn from_raw(body: Vec) -> Self { 137 | let cid = read_into_cid_v1_raw(&mut body.as_slice()).expect("Should be able to read vec"); 138 | CarBlock { cid, body } 139 | } 140 | 141 | /// Serializes value as dcbor42, and creates a new CarBlock with dcbor42 CID 142 | pub fn from_serializable(value: &T) -> Result { 143 | let body = 144 | serde_ipld_dagcbor::to_vec(value).map_err(|e| Error::Serialization(e.to_string()))?; 145 | let cid = read_into_cid_v1_cbor(&mut body.as_slice())?; 146 | Ok(CarBlock { cid, body }) 147 | } 148 | 149 | /// Read a single body block from a CAR file 150 | pub fn read_from(reader: &mut R) -> Result { 151 | // Read size 152 | let block_size = read_varint_usize(reader)?; 153 | // Wrap reader in byte counter reader 154 | let mut read_counter = ByteCounterReader::new(reader); 155 | // Read the cid 156 | let cid = Cid::read_bytes(&mut read_counter)?; 157 | // Get the number of bytes read while reading the cid 158 | let read_size = read_counter.read_size(); 159 | // Allocate memory for the body (the block length minus the CID length) 160 | let mut body = vec![0; block_size - read_size]; 161 | // Read data portion 162 | read_counter.read_exact(&mut body)?; 163 | Ok(Self::new(cid, body)?) 164 | } 165 | 166 | /// Write a single body block to a writer 167 | pub fn write_into(&self, writer: &mut W) -> Result { 168 | let cid_bytes = &self.cid.to_bytes(); 169 | let total_len = cid_bytes.len() + self.body.len(); 170 | // Write the length of the CID and data 171 | let written = write_usize_varint(writer, total_len)?; 172 | // Write CID 173 | writer.write_all(&cid_bytes)?; 174 | writer.write_all(&self.body)?; 175 | Ok(written + total_len) 176 | } 177 | 178 | /// Get the CID of the block 179 | pub fn cid(&self) -> &Cid { 180 | &self.cid 181 | } 182 | 183 | /// Get the body (data) of the block 184 | pub fn body(&self) -> &Vec { 185 | &self.body 186 | } 187 | } 188 | 189 | #[derive(Debug, Error)] 190 | pub enum Error { 191 | #[error("IO error: {0}")] 192 | Io(#[from] std::io::Error), 193 | #[error("Error decoding unsigned-varint: {0}")] 194 | UnsignedVarIntDecode(unsigned_varint::decode::Error), 195 | #[error("CID error: {0}")] 196 | Cid(cid::Error), 197 | #[error("Multihash error: {0}")] 198 | Multihash(multihash::Error), 199 | #[error("Serialization error: {0}")] 200 | Serialization(String), 201 | #[error("Invalid block: {0}")] 202 | InvalidBlock(String), 203 | #[error("Other error: {0}")] 204 | Other(String), 205 | } 206 | 207 | impl From for Error { 208 | fn from(err: crate::cid::Error) -> Self { 209 | match err { 210 | crate::cid::Error::Io(err) => Self::Io(err), 211 | crate::cid::Error::Multihash(err) => Self::Multihash(err), 212 | } 213 | } 214 | } 215 | 216 | impl From for Error { 217 | fn from(err: varint::Error) -> Self { 218 | match err { 219 | varint::Error::Io(err) => Error::Io(err), 220 | varint::Error::UnsignedVarIntDecode(err) => Error::UnsignedVarIntDecode(err), 221 | varint::Error::Other(msg) => Error::Other(msg), 222 | } 223 | } 224 | } 225 | 226 | impl From for Error { 227 | fn from(err: cid::Error) -> Self { 228 | match err { 229 | cid::Error::Io(err) => Self::Io(err), 230 | _ => Self::Cid(err), 231 | } 232 | } 233 | } 234 | 235 | impl From for Error { 236 | fn from(err: multihash::Error) -> Self { 237 | match err { 238 | multihash::Error::Io(err) => Self::Io(err), 239 | _ => Self::Multihash(err), 240 | } 241 | } 242 | } 243 | 244 | #[cfg(test)] 245 | mod tests { 246 | use super::*; 247 | 248 | #[test] 249 | fn test_car_roundtrip_with_tempfile() { 250 | use std::io::{Seek, SeekFrom}; 251 | use tempfile::tempfile; 252 | 253 | // Create a test header in CBOR format 254 | // For simplicity, we're creating a CAR v1 header with an empty roots array 255 | let header = CarHeader::new_v1(); 256 | 257 | // Create a temporary file 258 | let mut temp_file = tempfile().unwrap(); 259 | 260 | // Create writer and write header 261 | let mut car_writer = CarWriter::new(&mut temp_file, &header).unwrap(); 262 | 263 | // Write block 264 | let block_body = "Hello world"; 265 | let car_block = CarBlock::from_raw(block_body.as_bytes().to_vec()); 266 | car_block.write_into(&mut car_writer).unwrap(); 267 | 268 | let block_body_2 = "Hola world"; 269 | let car_block_2 = CarBlock::from_raw(block_body_2.as_bytes().to_vec()); 270 | car_block_2.write_into(&mut car_writer).unwrap(); 271 | 272 | // Reset file position to beginning 273 | temp_file.seek(SeekFrom::Start(0)).unwrap(); 274 | 275 | // Read the header back 276 | let car_reader: CarReader<_, CarHeader> = CarReader::read_from(&mut temp_file).unwrap(); 277 | 278 | // Verify the result 279 | assert_eq!(&header, car_reader.header()); 280 | 281 | let blocks: Result, Error> = car_reader.collect(); 282 | let blocks = blocks.unwrap(); 283 | assert_eq!(blocks.len(), 2); 284 | 285 | let block = blocks.first().unwrap(); 286 | assert_eq!(block.body(), block_body.as_bytes()); 287 | 288 | let block_2 = blocks.get(1).unwrap(); 289 | assert_eq!(block_2.body(), block_body_2.as_bytes()); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/car_claim_header.rs: -------------------------------------------------------------------------------- 1 | use crate::claim::Claim; 2 | use cid::Cid; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 6 | pub struct CarClaimHeader { 7 | version: u64, 8 | pub roots: Vec, 9 | pub claims: Vec, 10 | } 11 | 12 | impl CarClaimHeader { 13 | pub fn new(roots: Vec, claims: Vec) -> Self { 14 | CarClaimHeader { 15 | version: 1, 16 | roots, 17 | claims, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cid.rs: -------------------------------------------------------------------------------- 1 | use crate::multiformats::{MULTICODEC_DCBOR, MULTICODEC_RAW}; 2 | use crate::multihash::{self, read_into_multihash}; 3 | pub use cid::Cid; 4 | use std::io::{self, Read}; 5 | use thiserror::Error; 6 | 7 | /// Read bytes into a CID v1. 8 | pub fn read_into_cid_v1(codec: u64, reader: &mut R) -> Result { 9 | let hash = read_into_multihash(reader)?; 10 | Ok(Cid::new_v1(codec, hash)) 11 | } 12 | 13 | /// Read bytes into a CID v1 with a raw codec 14 | pub fn read_into_cid_v1_raw(reader: &mut R) -> Result { 15 | read_into_cid_v1(MULTICODEC_RAW, reader) 16 | } 17 | 18 | /// Read bytes into a CID v1 with a dag-cbor codec 19 | pub fn read_into_cid_v1_cbor(reader: &mut R) -> Result { 20 | read_into_cid_v1(MULTICODEC_DCBOR, reader) 21 | } 22 | 23 | #[derive(Error, Debug)] 24 | pub enum Error { 25 | #[error("IO error")] 26 | Io(#[from] io::Error), 27 | #[error("Multihash error")] 28 | Multihash(multihash::Error), 29 | } 30 | 31 | impl From for Error { 32 | fn from(err: multihash::Error) -> Self { 33 | match err { 34 | multihash::Error::Io(err) => Error::Io(err), 35 | _ => Error::Multihash(err), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/claim.rs: -------------------------------------------------------------------------------- 1 | use crate::did; 2 | use crate::ed25519::{self, Ed25519KeyMaterial}; 3 | use crate::{did::DidKey, util::now}; 4 | use cid::Cid; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::TryReserveError; 7 | use thiserror::Error; 8 | 9 | /// An SZDT Claim. 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 11 | pub struct Claim { 12 | payload: Payload, 13 | signature: Vec, 14 | } 15 | 16 | impl Claim { 17 | pub fn new(payload: Payload, signature: Vec) -> Self { 18 | Self { payload, signature } 19 | } 20 | 21 | pub fn payload(&self) -> &Payload { 22 | &self.payload 23 | } 24 | 25 | pub fn signature(&self) -> &Vec { 26 | &self.signature 27 | } 28 | 29 | /// Is claim valid? Checks signature and other properties, such as exp. 30 | pub fn validate(&self, now_time: Option) -> Result<(), Error> { 31 | if self.is_too_early(now_time) { 32 | return Err(Error::Nbf); 33 | } 34 | if self.is_expired(now_time) { 35 | return Err(Error::Exp); 36 | } 37 | self.check_signature()?; 38 | return Ok(()); 39 | } 40 | 41 | /// Check if signature is valid for claim 42 | pub fn check_signature(&self) -> Result<(), Error> { 43 | let public_key = self.payload.iss.pubkey(); 44 | let key_material = Ed25519KeyMaterial::try_from_public_key(public_key)?; 45 | let payload_bytes = Vec::try_from(self.payload())?; 46 | let result = key_material.verify(&payload_bytes, &self.signature())?; 47 | return Ok(result); 48 | } 49 | 50 | /// Is claim expired? 51 | pub fn is_expired(&self, now_time: Option) -> bool { 52 | match self.payload.exp { 53 | Some(exp) => exp < now_time.unwrap_or_else(now), 54 | None => false, 55 | } 56 | } 57 | 58 | /// Is claim too early? 59 | pub fn is_too_early(&self, now_time: Option) -> bool { 60 | match self.payload.nbf { 61 | Some(nbf) => nbf > now_time.unwrap_or_else(now), 62 | None => false, 63 | } 64 | } 65 | } 66 | 67 | /// Build a claim 68 | #[derive(Clone, Debug)] 69 | pub struct Builder { 70 | /// Issuer (DID) 71 | key_material: Ed25519KeyMaterial, 72 | /// Issued at (UNIX timestamp, seconds) 73 | iat: u64, 74 | /// Not valid before (UNIX timestamp, seconds) 75 | nbf: Option, 76 | /// Expiration time (UNIX timestamp, seconds) 77 | exp: Option, 78 | /// Assertions 79 | ast: Vec, 80 | } 81 | 82 | impl Builder { 83 | /// Build a claim, starting with the bytes of your secret key. 84 | pub fn new(private_key: &[u8]) -> Result { 85 | let key_material = Ed25519KeyMaterial::try_from_private_key(private_key)?; 86 | Ok(Self { 87 | key_material, 88 | iat: now(), 89 | nbf: Some(now()), 90 | exp: None, 91 | ast: Vec::new(), 92 | }) 93 | } 94 | 95 | pub fn iat(mut self, iat: u64) -> Self { 96 | self.iat = iat; 97 | self 98 | } 99 | 100 | pub fn nbf(mut self, nbf: u64) -> Self { 101 | self.nbf = Some(nbf); 102 | self 103 | } 104 | 105 | pub fn exp(mut self, exp: u64) -> Self { 106 | self.exp = Some(exp); 107 | self 108 | } 109 | 110 | pub fn ast(mut self, ast: Vec) -> Self { 111 | self.ast = ast; 112 | self 113 | } 114 | 115 | pub fn add_ast(mut self, ast: Assertion) -> Self { 116 | self.ast.push(ast); 117 | self 118 | } 119 | 120 | /// Sign and return the claim 121 | pub fn sign(self) -> Result { 122 | let pubkey = self.key_material.public_key(); 123 | let did = DidKey::new(&pubkey)?; 124 | let payload = Payload { 125 | iss: did, 126 | iat: self.iat, 127 | nbf: self.nbf, 128 | exp: self.exp, 129 | ast: self.ast, 130 | }; 131 | let payload_bytes: Vec = (&payload).try_into()?; 132 | let signature = self.key_material.sign(&payload_bytes)?; 133 | Ok(Claim { payload, signature }) 134 | } 135 | } 136 | 137 | /// A signed claim 138 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 139 | pub struct Payload { 140 | /// Issuer (DID) 141 | pub iss: DidKey, 142 | /// Issued at (UNIX timestamp, seconds) 143 | pub iat: u64, 144 | /// Not valid before (UNIX timestamp, seconds) 145 | pub nbf: Option, 146 | /// Expiration time (UNIX timestamp, seconds) 147 | pub exp: Option, 148 | /// Assertions 149 | pub ast: Vec, 150 | } 151 | 152 | impl Payload { 153 | /// Create a new payload 154 | pub fn new(iss: DidKey, iat: u64) -> Self { 155 | Self { 156 | iss, 157 | iat, 158 | nbf: None, 159 | exp: None, 160 | ast: Vec::new(), 161 | } 162 | } 163 | } 164 | 165 | impl TryFrom<&Payload> for Vec { 166 | type Error = Error; 167 | 168 | fn try_from(value: &Payload) -> Result { 169 | let bytes = serde_ipld_dagcbor::to_vec(value)?; 170 | Ok(bytes) 171 | } 172 | } 173 | 174 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 175 | #[serde(tag = "kind")] 176 | pub enum Assertion { 177 | #[serde(rename = "witness")] 178 | Witness(WitnessAssertion), 179 | } 180 | 181 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 182 | pub struct WitnessAssertion { 183 | pub cid: Cid, 184 | } 185 | 186 | #[derive(Debug, Error)] 187 | pub enum Error { 188 | #[error("Invalid signature: {0}")] 189 | Ed25519(#[from] ed25519::Error), 190 | #[error("dag-cbor encode error: {0}")] 191 | CborEncodeError(#[from] serde_ipld_dagcbor::EncodeError), 192 | #[error("DID error: {0}")] 193 | DidError(#[from] did::Error), 194 | #[error("Claim is too early")] 195 | Nbf, 196 | #[error("Claim expired")] 197 | Exp, 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn test_builder_sign_and_validate() { 206 | // Generate a key pair for testing 207 | let (_, privkey) = ed25519::generate_keypair(); 208 | 209 | // Build and sign a claim 210 | let claim = Builder::new(&privkey).unwrap().sign().unwrap(); 211 | 212 | // Validate the claim 213 | claim.validate(None).unwrap(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/did.rs: -------------------------------------------------------------------------------- 1 | use crate::base58btc; 2 | use crate::ed25519; 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | /// The multicodec prefix for ed25519 public key is 0xed. 7 | /// https://github.com/multiformats/multicodec/blob/master/table.csv 8 | const MULTICODEC_ED25519_PUB_PREFIX: u8 = 0xed; 9 | 10 | /// The prefix for did:key using Base58BTC encoding. 11 | /// The multibase code for ed25519 public key is 'z'. 12 | const DID_KEY_BASE58BTC_PREFIX: &str = "did:key:z"; 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 15 | pub struct DidKey(ed25519::PublicKey); 16 | 17 | impl DidKey { 18 | pub fn new(pubkey_bytes: &[u8]) -> Result { 19 | let pubkey = ed25519::to_public_key(pubkey_bytes)?; 20 | Ok(DidKey(pubkey)) 21 | } 22 | 23 | pub fn pubkey(&self) -> &ed25519::PublicKey { 24 | &self.0 25 | } 26 | } 27 | 28 | impl std::fmt::Display for DidKey { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | write!(f, "{}", String::from(self)) 31 | } 32 | } 33 | 34 | impl TryFrom<&str> for DidKey { 35 | type Error = Error; 36 | 37 | /// Parse a did:key str encoding an ed25519 public key into a DidKey. 38 | fn try_from(did_key: &str) -> Result { 39 | // Parse the did:key 40 | let base58_key = did_key 41 | .strip_prefix(DID_KEY_BASE58BTC_PREFIX) 42 | .ok_or(Error::UnsupportedBase)?; 43 | 44 | let decoded_bytes = base58btc::decode(base58_key)?; 45 | 46 | // Verify that the first byte corresponds to ED25519_PUB_PREFIX 47 | if decoded_bytes.is_empty() || decoded_bytes[0] != MULTICODEC_ED25519_PUB_PREFIX { 48 | return Err(Error::UnsupportedCodec); 49 | } 50 | 51 | // Extract the public key 52 | DidKey::new(&decoded_bytes[1..]) 53 | } 54 | } 55 | 56 | impl From<&DidKey> for String { 57 | fn from(did_key: &DidKey) -> Self { 58 | // Convert public key to multibase encoded string 59 | let mut multicodec_bytes = vec![MULTICODEC_ED25519_PUB_PREFIX]; 60 | multicodec_bytes.extend_from_slice(&did_key.0); 61 | 62 | // Encode with multibase (Base58BTC, prefix 'z') 63 | let multibase_encoded = base58btc::encode(multicodec_bytes); 64 | 65 | // Construct the did:key 66 | format!("{}{}", DID_KEY_BASE58BTC_PREFIX, multibase_encoded) 67 | } 68 | } 69 | 70 | impl Serialize for DidKey { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: serde::Serializer, 74 | { 75 | serializer.serialize_str(&String::from(self)) 76 | } 77 | } 78 | 79 | impl<'de> Deserialize<'de> for DidKey { 80 | fn deserialize(deserializer: D) -> Result 81 | where 82 | D: serde::Deserializer<'de>, 83 | { 84 | let s = String::deserialize(deserializer)?; 85 | DidKey::try_from(s.as_str()).map_err(|e| serde::de::Error::custom(e.to_string())) 86 | } 87 | } 88 | 89 | #[derive(Debug, Error)] 90 | pub enum Error { 91 | #[error("Public key error: {0}")] 92 | Key(#[from] ed25519::Error), 93 | #[error("Base encoding/decoding error: {0}")] 94 | Base(String), 95 | #[error("Unsupported base encoding. Only Base58BTC is supported.")] 96 | UnsupportedBase, 97 | #[error("Unsupported codec. Only Ed25519 public keys are supported.")] 98 | UnsupportedCodec, 99 | } 100 | 101 | impl From for Error { 102 | fn from(err: bs58::decode::Error) -> Self { 103 | Error::Base(err.to_string()) 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use super::*; 110 | 111 | #[test] 112 | fn test_roundtrip_ed25519_did_key() { 113 | // Test vector 114 | let pubkey: [u8; 32] = [ 115 | 215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, 116 | 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26, 117 | ]; 118 | 119 | let did = DidKey(pubkey); 120 | let did_string = String::from(&did); 121 | let did2 = DidKey::try_from(did_string.as_str()).unwrap(); 122 | 123 | assert_eq!(did, did2); 124 | } 125 | 126 | #[test] 127 | fn test_decode_invalid_did_key() { 128 | // Invalid prefix 129 | assert!(DidKey::try_from("did:invalid:z123").is_err()); 130 | 131 | // Invalid encoding 132 | assert!(DidKey::try_from("did:key:INVALID").is_err()); 133 | 134 | // Empty string 135 | assert!(DidKey::try_from("").is_err()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/ed25519.rs: -------------------------------------------------------------------------------- 1 | use ed25519_dalek::{ 2 | self, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SecretKey, Signature, Signer, SigningKey, Verifier, 3 | VerifyingKey, 4 | }; 5 | use rand::rngs::OsRng; 6 | use thiserror::Error; 7 | 8 | pub type PublicKey = [u8; PUBLIC_KEY_LENGTH]; 9 | pub type SignatureBytes = [u8; 64]; 10 | 11 | /// Wraps ed25519 key material, allowing you to sign and verify content 12 | #[derive(Debug, Clone)] 13 | pub struct Ed25519KeyMaterial { 14 | public_key: PublicKey, 15 | private_key: Option, 16 | } 17 | 18 | impl Ed25519KeyMaterial { 19 | /// Initialize from private key bytes 20 | pub fn try_from_private_key(privkey: &[u8]) -> Result { 21 | let secret_key = to_secret_key(privkey)?; 22 | let signing_key = SigningKey::from_bytes(&secret_key); 23 | Ok(Self { 24 | public_key: signing_key.verifying_key().to_bytes(), 25 | private_key: Some(signing_key.to_bytes()), 26 | }) 27 | } 28 | 29 | /// Construct key material from a publick key, without a private key 30 | pub fn try_from_public_key(pubkey: &[u8]) -> Result { 31 | let public_key = to_public_key(pubkey)?; 32 | Ok(Self { 33 | public_key, 34 | private_key: None, 35 | }) 36 | } 37 | 38 | /// Get the public key portion 39 | pub fn public_key(&self) -> Vec { 40 | self.public_key.to_vec() 41 | } 42 | 43 | /// Sign payload, returning signature bytes 44 | pub fn sign(&self, payload: &[u8]) -> Result, Error> { 45 | match &self.private_key { 46 | Some(private_key) => { 47 | let signing_key = SigningKey::from_bytes(private_key); 48 | Ok(signing_key.sign(payload).to_bytes().to_vec()) 49 | } 50 | None => Err(Error::SigningError( 51 | "Can't sign payload. No private key.".to_string(), 52 | )), 53 | } 54 | } 55 | 56 | /// Verify signature 57 | pub fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<(), Error> { 58 | let signature = Signature::from_slice(signature)?; 59 | let verifying_key = VerifyingKey::from_bytes(&self.public_key)?; 60 | verifying_key.verify(payload, &signature)?; 61 | Ok(()) 62 | } 63 | } 64 | 65 | impl From for Ed25519KeyMaterial { 66 | fn from(signing_key: SigningKey) -> Self { 67 | Self { 68 | public_key: signing_key.verifying_key().to_bytes(), 69 | private_key: Some(signing_key.to_bytes()), 70 | } 71 | } 72 | } 73 | 74 | /// Generate a new signing keypair. 75 | /// Returns a tuple of `(pubkey, privkey)`. 76 | pub fn generate_keypair() -> (Vec, Vec) { 77 | let mut csprng = OsRng; 78 | let signing_key = SigningKey::generate(&mut csprng); 79 | ( 80 | signing_key.verifying_key().to_bytes().to_vec(), 81 | signing_key.to_bytes().to_vec(), 82 | ) 83 | } 84 | 85 | /// Convert a Vec to PublicKey. 86 | /// Returns an error if the input is not exactly 32 bytes. 87 | pub fn to_public_key(bytes: &[u8]) -> Result { 88 | if bytes.len() != PUBLIC_KEY_LENGTH { 89 | return Err(Error::InvalidPublicKey(format!( 90 | "Public key must be {} bytes, got {}", 91 | PUBLIC_KEY_LENGTH, 92 | bytes.len() 93 | ))); 94 | } 95 | 96 | let mut public_key = [0u8; PUBLIC_KEY_LENGTH]; 97 | public_key.copy_from_slice(bytes); 98 | Ok(public_key) 99 | } 100 | 101 | /// Convert a Vec to PrivateKey. 102 | /// Returns an error if the input is not exactly 32 bytes. 103 | fn to_secret_key(bytes: &[u8]) -> Result { 104 | if bytes.len() != SECRET_KEY_LENGTH { 105 | return Err(Error::InvalidPrivateKey(format!( 106 | "Private key must be {} bytes, got {}", 107 | SECRET_KEY_LENGTH, 108 | bytes.len() 109 | ))); 110 | } 111 | 112 | let mut private_key = [0u8; SECRET_KEY_LENGTH]; 113 | private_key.copy_from_slice(bytes); 114 | Ok(private_key) 115 | } 116 | 117 | #[derive(Debug, Error)] 118 | pub enum Error { 119 | #[error("Ed25519 error: {0}")] 120 | Ed25519Error(#[from] ed25519_dalek::ed25519::Error), 121 | #[error("Invalid public key: {0}")] 122 | InvalidPublicKey(String), 123 | #[error("Invalid private key: {0}")] 124 | InvalidPrivateKey(String), 125 | #[error("Can't sign payload: {0}")] 126 | SigningError(String), 127 | #[error("Invalid signature: {0}")] 128 | InvalidSignature(String), 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | #[test] 136 | fn test_ed25519_key_material_roundtrip() { 137 | // Generate a signing key 138 | let (pubkey, privkey) = generate_keypair(); 139 | 140 | // Create Ed25519KeyMaterial from the signing key 141 | let key_material = Ed25519KeyMaterial::try_from_private_key(&privkey).unwrap(); 142 | 143 | // Test signing and verification 144 | let message = b"test message for roundtrip verification"; 145 | let signature = key_material.sign(message).unwrap(); 146 | 147 | let key_material_2 = Ed25519KeyMaterial::try_from_public_key(&pubkey).unwrap(); 148 | 149 | // Verify using the same key material 150 | let result = key_material_2.verify(message, &signature); 151 | assert!(result.is_ok()); 152 | 153 | // Try to verify with a different message 154 | let wrong_message = b"wrong message".to_vec(); 155 | let result = key_material_2.verify(&wrong_message, &signature); 156 | assert!(result.is_err()); 157 | } 158 | 159 | #[test] 160 | fn test_vec_to_public_key() { 161 | // Valid case 162 | let valid_bytes = vec![0u8; PUBLIC_KEY_LENGTH]; 163 | let result = to_public_key(&valid_bytes); 164 | assert!(result.is_ok()); 165 | 166 | // Invalid case - wrong length 167 | let invalid_bytes = vec![0u8; PUBLIC_KEY_LENGTH - 1]; 168 | let result = to_public_key(&invalid_bytes); 169 | assert!(result.is_err()); 170 | } 171 | 172 | #[test] 173 | fn test_vec_to_private_key() { 174 | // Valid case 175 | let valid_bytes = vec![0u8; SECRET_KEY_LENGTH]; 176 | let result = to_secret_key(&valid_bytes); 177 | assert!(result.is_ok()); 178 | 179 | // Invalid case - wrong length 180 | let invalid_bytes = vec![0u8; SECRET_KEY_LENGTH - 1]; 181 | let result = to_secret_key(&invalid_bytes); 182 | assert!(result.is_err()); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | IoError(String, std::io::Error), 4 | Ed25519Error(String, ed25519_dalek::ed25519::Error), 5 | DecodingError(String), 6 | ValidationError(String), 7 | SignatureVerificationError(String), 8 | ValueError(String), 9 | } 10 | 11 | impl std::error::Error for Error { 12 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 13 | match self { 14 | Error::IoError(_, err) => Some(err), 15 | Error::Ed25519Error(_, err) => Some(err), 16 | Error::DecodingError(_) => None, 17 | Error::ValidationError(_) => None, 18 | Error::SignatureVerificationError(_) => None, 19 | Error::ValueError(_) => None, 20 | } 21 | } 22 | } 23 | 24 | impl std::fmt::Display for Error { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | Error::IoError(msg, _) => write!(f, "IO error: {}", msg), 28 | Error::Ed25519Error(msg, _) => write!(f, "Ed25519 error: {}", msg), 29 | Error::DecodingError(msg) => write!(f, "Decoding error: {}", msg), 30 | Error::ValidationError(msg) => write!(f, "Validation error: {}", msg), 31 | Error::SignatureVerificationError(msg) => write!(f, "Signature error: {}", msg), 32 | Error::ValueError(msg) => write!(f, "Value error: {}", msg), 33 | } 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(err: std::io::Error) -> Self { 39 | Error::IoError(err.to_string(), err) 40 | } 41 | } 42 | 43 | impl From for Error { 44 | fn from(err: data_encoding::DecodeError) -> Self { 45 | Error::DecodingError(err.to_string()) 46 | } 47 | } 48 | 49 | impl From for Error { 50 | fn from(err: bs58::decode::Error) -> Self { 51 | Error::DecodingError(err.to_string()) 52 | } 53 | } 54 | 55 | impl From for Error { 56 | fn from(err: ed25519_dalek::ed25519::Error) -> Self { 57 | Error::Ed25519Error(err.to_string(), err) 58 | } 59 | } 60 | 61 | pub type Result = std::result::Result; 62 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | 5 | /// Recursively walks a directory and returns all file paths found. 6 | pub fn walk_files(dir: &Path) -> Result, io::Error> { 7 | let mut paths = Vec::new(); 8 | _walk_files(&mut paths, dir)?; 9 | Ok(paths) 10 | } 11 | 12 | fn _walk_files(paths: &mut Vec, path: &Path) -> Result<(), io::Error> { 13 | if path.is_dir() { 14 | // Iterate over directory entries 15 | for child in fs::read_dir(path)? { 16 | let child = child?; 17 | _walk_files(paths, &child.path())?; 18 | } 19 | } else { 20 | // Add the entry itself 21 | paths.push(path.to_path_buf()); 22 | } 23 | Ok(()) 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | use std::fs; 30 | use tempfile::tempdir; 31 | 32 | #[test] 33 | fn test_walk_dir() -> Result<(), io::Error> { 34 | // Create a temporary directory structure 35 | let temp_dir = tempdir()?; 36 | let temp_path = temp_dir.path(); 37 | 38 | // Create some nested directories and files 39 | let subdir1 = temp_path.join("subdir1"); 40 | let subdir2 = temp_path.join("subdir1/subdir2"); 41 | fs::create_dir(&subdir1)?; 42 | fs::create_dir(&subdir2)?; 43 | 44 | // Create some files 45 | fs::write(temp_path.join("file1.txt"), b"content1")?; 46 | fs::write(subdir1.join("file2.txt"), b"content2")?; 47 | fs::write(subdir2.join("file3.txt"), b"content3")?; 48 | 49 | // Test walk_dir 50 | let paths = walk_files(temp_path)?; 51 | 52 | // Check that we have the expected number of paths 53 | assert_eq!(paths.len(), 3); // root dir + 2 subdirs + 3 files 54 | 55 | // Check that specific paths exist in the result 56 | assert!(paths.contains(&temp_path.join("file1.txt"))); 57 | assert!(paths.contains(&subdir1.join("file2.txt"))); 58 | assert!(paths.contains(&subdir2.join("file3.txt"))); 59 | 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod base58btc; 2 | pub mod byte_counter_reader; 3 | pub mod car; 4 | pub mod car_claim_header; 5 | pub mod cid; 6 | pub mod claim; 7 | pub mod did; 8 | pub mod ed25519; 9 | pub mod error; 10 | pub mod file; 11 | pub mod manifest; 12 | pub mod multiformats; 13 | pub mod multihash; 14 | pub mod util; 15 | pub mod varint; 16 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use crate::cid::{self, read_into_cid_v1_raw}; 2 | use crate::file::walk_files; 3 | use cid::Cid; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::BTreeMap; 6 | use std::collections::TryReserveError; 7 | use std::fs::File; 8 | use std::io::Read; 9 | use std::path::{Path, PathBuf}; 10 | use thiserror::Error; 11 | use url::Url; 12 | 13 | /// Archive manifest 14 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 15 | pub struct Manifest { 16 | files: BTreeMap, 17 | } 18 | 19 | impl Manifest { 20 | pub fn new() -> Self { 21 | Manifest { 22 | files: BTreeMap::new(), 23 | } 24 | } 25 | 26 | /// Create an archive from a directory 27 | pub fn from_dir(dir: &Path) -> Result { 28 | let mut archive = Manifest::new(); 29 | archive.add_dir(dir)?; 30 | Ok(archive) 31 | } 32 | 33 | /// Add a file entry by reading from the file system, generating a CID. 34 | /// Uses file path (relative to working directory) as the display name. 35 | pub fn add_file(&mut self, path: &Path) -> Result<(), Error> { 36 | let mut file = File::open(path)?; 37 | let link = Link::read_from(&mut file)?; 38 | self.files.insert(path.to_owned(), link); 39 | Ok(()) 40 | } 41 | 42 | /// Add all files in a directory (recursive) 43 | pub fn add_dir(&mut self, dir: &Path) -> Result<(), Error> { 44 | for path in walk_files(dir)? { 45 | self.add_file(&path)?; 46 | } 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl TryFrom for Cid { 52 | type Error = Error; 53 | 54 | fn try_from(value: Manifest) -> Result { 55 | let bytes = serde_ipld_dagcbor::to_vec(&value)?; 56 | let archive_cid = cid::read_into_cid_v1_cbor(&mut bytes.as_slice())?; 57 | Ok(archive_cid) 58 | } 59 | } 60 | 61 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 62 | pub struct Link { 63 | pub content: Cid, 64 | pub location: Vec, 65 | } 66 | 67 | impl Link { 68 | pub fn new(content: Cid, location: Vec) -> Self { 69 | Link { content, location } 70 | } 71 | 72 | /// Read link from reader 73 | pub fn read_from(reader: &mut R) -> Result { 74 | let cid = read_into_cid_v1_raw(reader)?; 75 | let link = Link::new(cid, Vec::new()); 76 | Ok(link) 77 | } 78 | } 79 | 80 | #[derive(Debug, Error)] 81 | pub enum Error { 82 | #[error("I/O error: {0}")] 83 | Io(#[from] std::io::Error), 84 | #[error("CID error: {0}")] 85 | Cid(#[from] cid::Error), 86 | #[error("CBOR encode error: {0}")] 87 | CborEncode(#[from] serde_ipld_dagcbor::EncodeError), 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | 94 | #[test] 95 | fn test_try_from_archive_to_cid() { 96 | let mut archive = Manifest::new(); 97 | 98 | // Create a mock file entry 99 | let link = Link { 100 | content: Cid::try_from("bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") 101 | .unwrap(), 102 | location: Vec::new(), 103 | }; 104 | 105 | archive.files.insert("test-file".into(), link); 106 | 107 | // Convert archive to CID 108 | let cid = Cid::try_from(archive).unwrap(); 109 | assert!(cid.to_string().starts_with("bafy")); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/multiformats.rs: -------------------------------------------------------------------------------- 1 | /// Constants for multiformats. 2 | /// See 3 | 4 | pub const MULTICODEC_RAW: u64 = 0x55; 5 | pub const MULTICODEC_DCBOR: u64 = 0x71; 6 | pub const MULTIHASH_SHA256: u64 = 0x12; 7 | -------------------------------------------------------------------------------- /src/multihash.rs: -------------------------------------------------------------------------------- 1 | use crate::multiformats::MULTIHASH_SHA256; 2 | use multihash::Multihash; 3 | use sha2::{Digest, Sha256}; 4 | use std::io::{self, Read}; 5 | use thiserror::Error; 6 | 7 | pub type Sha256Digest = [u8; 32]; 8 | pub type Multihash64 = Multihash<64>; 9 | 10 | /// Streaming read and hash bytes into a SHA-256 hash. 11 | pub fn read_into_sha256(reader: &mut R) -> Result { 12 | let mut hasher = Sha256::new(); 13 | io::copy(reader, &mut hasher)?; 14 | let hash = hasher.finalize(); 15 | try_into_hash(hash.as_slice()) 16 | } 17 | 18 | /// Streaming read and hash bytes into a SHA-256 multihash. 19 | pub fn read_into_multihash(reader: &mut R) -> Result { 20 | let hash = read_into_sha256(reader)?; 21 | into_multihash(&hash) 22 | } 23 | 24 | /// Converts a Slice to a fixed-size array of 32 bytes. 25 | pub fn try_into_hash(bytes: &[u8]) -> Result { 26 | let bytes_32 = bytes 27 | .try_into() 28 | .map_err(|_| Error::ValueError("Failed to convert slice into 32-byte array".to_string()))?; 29 | Ok(bytes_32) 30 | } 31 | 32 | pub fn into_multihash(hash: &[u8]) -> Result { 33 | let multihash = Multihash::<64>::wrap(MULTIHASH_SHA256, hash)?; 34 | Ok(multihash) 35 | } 36 | 37 | #[derive(Debug, Error)] 38 | pub enum Error { 39 | #[error("I/O error: {0}")] 40 | Io(#[from] io::Error), 41 | #[error("Multihash error: {0}")] 42 | Multihash(#[from] multihash::Error), 43 | #[error("Value error: {0}")] 44 | ValueError(String), 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_try_into_hash() { 53 | let data = [0u8; 32]; 54 | let result = try_into_hash(&data); 55 | assert!(result.is_ok()); 56 | assert_eq!(result.unwrap(), data); 57 | 58 | let data_short = [0u8; 31]; 59 | let result = try_into_hash(&data_short); 60 | assert!(result.is_err()); 61 | 62 | let data_long = [0u8; 33]; 63 | let result = try_into_hash(&data_long); 64 | assert!(result.is_err()); 65 | } 66 | 67 | #[test] 68 | fn test_into_multihash() { 69 | let data = [0u8; 32]; 70 | let result = into_multihash(&data); 71 | assert!(result.is_ok()); 72 | 73 | let multihash = result.unwrap(); 74 | assert_eq!(multihash.code(), MULTIHASH_SHA256); 75 | assert_eq!(multihash.digest(), &data); 76 | } 77 | 78 | #[test] 79 | fn test_read_into_sha256() { 80 | let data = b"hello world"; 81 | let result = read_into_sha256(&mut data.as_slice()); 82 | assert!(result.is_ok()); 83 | } 84 | 85 | #[test] 86 | fn test_read_into_multihash() { 87 | let data = b"hello world"; 88 | let mut cursor = std::io::Cursor::new(data); 89 | 90 | let result = read_into_multihash(&mut cursor); 91 | assert!(result.is_ok()); 92 | 93 | let multihash = result.unwrap(); 94 | assert_eq!(multihash.code(), MULTIHASH_SHA256); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | /// Get the current epoch time in seconds 4 | pub fn now() -> u64 { 5 | SystemTime::now() 6 | .duration_since(UNIX_EPOCH) 7 | .expect("Expected now to be greater than epoch") 8 | .as_secs() 9 | } 10 | -------------------------------------------------------------------------------- /src/varint.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use thiserror::Error; 3 | 4 | /// Read a leb128 (unsigned-varint) as a usize from a reader. 5 | pub fn read_varint_usize(reader: &mut impl std::io::Read) -> Result { 6 | let size = unsigned_varint::io::read_usize(reader)?; 7 | Ok(size) 8 | } 9 | 10 | /// Write a usize as a leb128 (unsigned-varint) to a writer. 11 | /// Returns the number of bytes written. 12 | pub fn write_usize_varint(writer: &mut impl std::io::Write, value: usize) -> Result { 13 | let mut buf = unsigned_varint::encode::usize_buffer(); 14 | let to_write = unsigned_varint::encode::usize(value, &mut buf); 15 | writer.write_all(&to_write)?; 16 | Ok(to_write.len()) 17 | } 18 | 19 | #[derive(Debug, Error)] 20 | pub enum Error { 21 | #[error("I/O error: {0}")] 22 | Io(#[from] io::Error), 23 | #[error("Error decoding unsigned varint: {0}")] 24 | UnsignedVarIntDecode(unsigned_varint::decode::Error), 25 | #[error("Error: {0}")] 26 | Other(String), 27 | } 28 | 29 | impl From for Error { 30 | fn from(err: unsigned_varint::io::ReadError) -> Self { 31 | match err { 32 | unsigned_varint::io::ReadError::Io(err) => Error::Io(err), 33 | unsigned_varint::io::ReadError::Decode(err) => Error::UnsignedVarIntDecode(err), 34 | _ => Error::Other(format!("Unknown error: {}", err)), 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | use std::io::Cursor; 43 | 44 | #[test] 45 | fn test_read_varint_usize() { 46 | // Test case 1: Single byte varint (value 1) 47 | let data = vec![0x01]; 48 | let mut reader = Cursor::new(data); 49 | let result = read_varint_usize(&mut reader).unwrap(); 50 | assert_eq!(result, 1); 51 | 52 | // Test case 2: Two byte varint (value 128) 53 | let data = vec![0x80, 0x01]; 54 | let mut reader = Cursor::new(data); 55 | let result = read_varint_usize(&mut reader).unwrap(); 56 | assert_eq!(result, 128); 57 | 58 | // Test case 3: Multi-byte varint (value 300) 59 | let data = vec![0xAC, 0x02]; 60 | let mut reader = Cursor::new(data); 61 | let result = read_varint_usize(&mut reader).unwrap(); 62 | assert_eq!(result, 300); 63 | 64 | // Test case 4: Empty reader should fail 65 | let data = vec![]; 66 | let mut reader = Cursor::new(data); 67 | let result = read_varint_usize(&mut reader); 68 | assert!(result.is_err()); 69 | } 70 | 71 | #[test] 72 | fn test_read_varint_usize_consumes_only_needed_bytes() { 73 | // Prepare data with a varint followed by other data 74 | let data = vec![0x42, 0xFF, 0xFF]; // 0x42 is a single-byte varint (66), followed by other bytes 75 | let mut reader = Cursor::new(data); 76 | 77 | // Read the varint 78 | let result = read_varint_usize(&mut reader).unwrap(); 79 | 80 | // Verify the correct value was read 81 | assert_eq!(result, 66); 82 | 83 | // Check that only one byte was consumed by checking the position 84 | assert_eq!(reader.position(), 1); 85 | 86 | // Test with a multi-byte varint 87 | let data = vec![0x80, 0x01, 0xFF, 0xFF]; // Two-byte varint (128) followed by other bytes 88 | let mut reader = Cursor::new(data); 89 | 90 | let result = read_varint_usize(&mut reader).unwrap(); 91 | 92 | assert_eq!(result, 128); 93 | assert_eq!(reader.position(), 2); // Should have consumed exactly 2 bytes 94 | } 95 | 96 | #[test] 97 | fn test_write_usize_varint() { 98 | let mut buf: Vec = Vec::new(); 99 | write_usize_varint(&mut buf, 128).unwrap(); 100 | assert_eq!(buf, vec![0x80, 0x01]); 101 | } 102 | } 103 | --------------------------------------------------------------------------------