diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5749bc95b..df25bfc8c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,7 +34,9 @@ jobs: steps.release.outputs['rust/noosphere-into--release_created'] || steps.release.outputs['rust/noosphere-ns--release_created'] || steps.release.outputs['rust/noosphere-sphere--release_created'] || - steps.release.outputs['rust/noosphere-storage--release_created'] }} + steps.release.outputs['rust/noosphere-storage--release_created'] || + steps.release.outputs['rust/noosphere-ucan--release_created'] || + steps.release.outputs['rust/noosphere-ucan-key-support--release_created'] }} steps: - uses: chainguard-dev/actions/setup-gitsign@main - name: 'Run release-please' diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b96dfe9cd..aea7cd182 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -8,5 +8,7 @@ "rust/noosphere": "0.16.1", "rust/noosphere-ipfs": "0.8.6", "rust/noosphere-gateway": "0.12.0", - "rust/noosphere-common": "0.1.2" -} \ No newline at end of file + "rust/noosphere-common": "0.1.2", + "rust/noosphere-ucan": "0.4.0", + "rust/noosphere-ucan-key-support": "0.1.7" +} diff --git a/Cargo.lock b/Cargo.lock index 1d0e313c9..a2407b95a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,17 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.7" @@ -300,6 +289,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "attohttpc" version = "0.24.1" @@ -446,12 +444,24 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -496,6 +506,18 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -558,6 +580,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bls12_381_plus" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c681aa947677ec0c5ccfa6f14c0dd039ddbaa7b12952bf146bd5226a5f9880" +dependencies = [ + "digest 0.9.0", + "ff 0.12.1", + "group 0.12.1", + "heapless", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + [[package]] name = "bs58" version = "0.5.0" @@ -799,7 +844,7 @@ dependencies = [ "http 0.2.11", "mime", "mime_guess", - "rand", + "rand 0.8.5", "thiserror", ] @@ -886,6 +931,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "crossbeam-channel" version = "0.5.11" @@ -916,6 +967,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -939,6 +1002,26 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "cs_serde_bytes" version = "0.12.2" @@ -1079,6 +1162,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.8" @@ -1144,6 +1237,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "did-key" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed21f9ed50f9d3f79b6ba84f2cf8a536399c3500bc99406c1bbc1e0c598524e" +dependencies = [ + "arrayref", + "base64 0.13.1", + "bls12_381_plus", + "bs58 0.4.0", + "curve25519-dalek 3.2.0", + "did_url", + "ed25519-dalek 1.0.1", + "getrandom 0.2.12", + "hkdf 0.11.0", + "json-patch", + "libsecp256k1", + "p256 0.11.1", + "serde", + "serde_json", + "sha2 0.9.9", + "x25519-dalek 1.1.1", +] + +[[package]] +name = "did_url" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d5f6334e473e3bb5650ab4ef3e4c910296b76968e62758e7c66157ff767c05" +dependencies = [ + "form_urlencoded", +] + [[package]] name = "digest" version = "0.9.0" @@ -1223,18 +1349,39 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.8", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", ] [[package]] @@ -1243,8 +1390,23 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "serde", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519 1.5.3", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", ] [[package]] @@ -1254,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek 4.1.2", - "ed25519", + "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.8", @@ -1264,16 +1426,17 @@ dependencies = [ [[package]] name = "ed25519-zebra" -version = "3.1.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ - "curve25519-dalek 3.2.0", - "hashbrown 0.12.3", + "curve25519-dalek 4.1.2", + "ed25519 2.2.3", + "hashbrown", "hex", "rand_core 0.6.4", "serde", - "sha2 0.9.9", + "sha2 0.10.8", "zeroize", ] @@ -1283,22 +1446,43 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "hkdf 0.12.4", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.0", "generic-array", - "group", + "group 0.13.0", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -1402,6 +1586,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.0" @@ -1453,6 +1648,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1629,6 +1830,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1638,7 +1850,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1734,13 +1946,24 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -1784,12 +2007,12 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "hash32" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" dependencies = [ - "ahash 0.7.7", + "byteorder", ] [[package]] @@ -1798,7 +2021,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.7", + "ahash", "allocator-api2", ] @@ -1808,7 +2031,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", "http 1.0.0", @@ -1826,6 +2049,19 @@ dependencies = [ "http 1.0.0", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -1866,7 +2102,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "socket2", "thiserror", "tinyvec", @@ -1888,7 +2124,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot 0.12.1", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -1896,13 +2132,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", ] [[package]] @@ -1914,6 +2180,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + [[package]] name = "home" version = "0.5.9" @@ -2168,7 +2445,7 @@ dependencies = [ "http 0.2.11", "hyper 0.14.28", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -2181,7 +2458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -2318,6 +2595,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3fa5a61630976fc4c353c70297f2e93f1930e3ccee574d59d618ccbd5154ce" +dependencies = [ + "serde", + "serde_json", + "treediff", +] + [[package]] name = "keccak" version = "0.1.5" @@ -2464,7 +2752,7 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom", + "getrandom 0.2.12", "instant", "libp2p-allow-block-list", "libp2p-connection-limits", @@ -2532,7 +2820,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project", "quick-protobuf", - "rand", + "rand 0.8.5", "rw-stream-sink", "serde", "smallvec", @@ -2565,14 +2853,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d665144a616dadebdc5fff186b1233488cdcd8bfb1223218ff084b6d052c94f7" dependencies = [ "asynchronous-codec", - "base64", + "base64 0.21.7", "byteorder", "bytes", "either", "fnv", "futures", "futures-ticker", - "getrandom", + "getrandom 0.2.12", "hex_fmt", "instant", "libp2p-core", @@ -2581,7 +2869,7 @@ dependencies = [ "prometheus-client", "quick-protobuf", "quick-protobuf-codec", - "rand", + "rand 0.8.5", "regex", "serde", "sha2 0.10.8", @@ -2619,12 +2907,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "999ec70441b2fb35355076726a6bc466c932e9bdc66f6a11c6c0aa17c7ab9be0" dependencies = [ - "bs58", - "ed25519-dalek", - "hkdf", + "bs58 0.5.0", + "ed25519-dalek 2.1.1", + "hkdf 0.12.4", "multihash 0.19.1", "quick-protobuf", - "rand", + "rand 0.8.5", "serde", "sha2 0.10.8", "thiserror", @@ -2652,7 +2940,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand", + "rand 0.8.5", "serde", "sha2 0.10.8", "smallvec", @@ -2675,7 +2963,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand", + "rand 0.8.5", "smallvec", "socket2", "tokio", @@ -2716,13 +3004,13 @@ dependencies = [ "multihash 0.19.1", "once_cell", "quick-protobuf", - "rand", + "rand 0.8.5", "sha2 0.10.8", "snow", "static_assertions", "thiserror", "tracing", - "x25519-dalek", + "x25519-dalek 2.0.1", "zeroize", ] @@ -2741,7 +3029,7 @@ dependencies = [ "libp2p-tls", "parking_lot 0.12.1", "quinn", - "rand", + "rand 0.8.5", "ring 0.16.20", "rustls", "socket2", @@ -2766,7 +3054,7 @@ dependencies = [ "libp2p-swarm-derive", "multistream-select", "once_cell", - "rand", + "rand 0.8.5", "smallvec", "tokio", "tracing", @@ -2879,6 +3167,54 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "libz-sys" version = "1.1.15" @@ -2924,7 +3260,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -3033,7 +3369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -3279,8 +3615,10 @@ dependencies = [ "noosphere-ipfs", "noosphere-ns", "noosphere-storage", + "noosphere-ucan", + "noosphere-ucan-key-support", "pkg-version", - "rand", + "rand 0.8.5", "reqwest", "rexie", "safer-ffi", @@ -3292,8 +3630,6 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "ucan", - "ucan-key-support", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -3322,8 +3658,10 @@ dependencies = [ "noosphere-ipfs", "noosphere-ns", "noosphere-storage", + "noosphere-ucan", + "noosphere-ucan-key-support", "pathdiff", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -3336,8 +3674,6 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", - "ucan", - "ucan-key-support", "url", "vergen", "vergen-pretty", @@ -3377,7 +3713,7 @@ dependencies = [ "futures-util", "gloo-timers", "instant", - "rand", + "rand 0.8.5", "tokio", "tokio-stream", "tracing", @@ -3396,7 +3732,7 @@ dependencies = [ "async-stream", "async-trait", "axum-extra", - "base64", + "base64 0.21.7", "bytes", "cid", "console_error_panic_hook", @@ -3404,7 +3740,7 @@ dependencies = [ "fastcdc", "futures", "futures-util", - "getrandom", + "getrandom 0.2.12", "gloo-net", "http 1.0.0", "instant", @@ -3415,15 +3751,17 @@ dependencies = [ "noosphere-collections", "noosphere-common", "noosphere-storage", + "noosphere-ucan", + "noosphere-ucan-key-support", "once_cell", - "rand", + "rand 0.8.5", "reqwest", "sentry-tracing", "serde", "serde_bytes", "serde_json", "serde_urlencoded", - "strum 0.25.0", + "strum", "strum_macros", "thiserror", "tiny-bip39", @@ -3433,8 +3771,6 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-wasm", - "ucan", - "ucan-key-support", "url", "wasm-bindgen", "wasm-bindgen-test", @@ -3463,10 +3799,12 @@ dependencies = [ "noosphere-ipfs", "noosphere-ns", "noosphere-storage", + "noosphere-ucan", + "noosphere-ucan-key-support", "reqwest", "serde", "serde_json", - "strum 0.25.0", + "strum", "strum_macros", "thiserror", "tokio", @@ -3475,8 +3813,6 @@ dependencies = [ "tower", "tower-http", "tracing", - "ucan", - "ucan-key-support", "url", "wasm-bindgen", ] @@ -3497,6 +3833,8 @@ dependencies = [ "libipld-cbor", "noosphere-core", "noosphere-storage", + "noosphere-ucan", + "noosphere-ucan-key-support", "subtext", "tempfile", "tokio", @@ -3504,8 +3842,6 @@ dependencies = [ "tokio-util", "tower-http", "tracing", - "ucan", - "ucan-key-support", "url", "wasm-bindgen-test", ] @@ -3529,7 +3865,8 @@ dependencies = [ "noosphere-common", "noosphere-core", "noosphere-storage", - "rand", + "noosphere-ucan", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -3537,7 +3874,6 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "ucan", "url", ] @@ -3561,7 +3897,9 @@ dependencies = [ "noosphere-core", "noosphere-ipfs", "noosphere-storage", - "rand", + "noosphere-ucan", + "noosphere-ucan-key-support", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -3571,8 +3909,6 @@ dependencies = [ "toml 0.8.10", "tower-http", "tracing", - "ucan", - "ucan-key-support", "url", "void", ] @@ -3584,7 +3920,7 @@ dependencies = [ "anyhow", "async-stream", "async-trait", - "base64", + "base64 0.21.7", "cid", "instant", "js-sys", @@ -3592,7 +3928,8 @@ dependencies = [ "libipld-core", "noosphere-common", "noosphere-core", - "rand", + "noosphere-ucan", + "rand 0.8.5", "rexie", "rocksdb", "serde", @@ -3602,7 +3939,6 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "ucan", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -3611,6 +3947,60 @@ dependencies = [ "witty-phrase-generator", ] +[[package]] +name = "noosphere-ucan" +version = "0.4.0" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "base64 0.21.7", + "bs58 0.5.0", + "cid", + "did-key", + "futures", + "getrandom 0.2.12", + "instant", + "libipld-core", + "libipld-json", + "log", + "rand 0.8.5", + "serde", + "serde_ipld_dagcbor", + "serde_json", + "strum", + "strum_macros", + "tokio", + "unsigned-varint 0.8.0", + "url", + "wasm-bindgen-test", +] + +[[package]] +name = "noosphere-ucan-key-support" +version = "0.1.7" +dependencies = [ + "anyhow", + "async-trait", + "bs58 0.5.0", + "ed25519-zebra", + "getrandom 0.2.12", + "js-sys", + "log", + "noosphere-ucan", + "npm_rs", + "p256 0.13.2", + "pollster", + "rand 0.8.5", + "rsa", + "sha2 0.10.8", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "npm_rs" version = "1.0.0" @@ -3653,7 +4043,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -3775,7 +4165,7 @@ dependencies = [ "opentelemetry", "ordered-float", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", ] @@ -3800,18 +4190,38 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2 0.10.8", ] +[[package]] +name = "pairing" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" +dependencies = [ + "group 0.12.1", +] + [[package]] name = "parking" version = "2.2.0" @@ -3893,7 +4303,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ - "base64", + "base64 0.21.7", "serde", ] @@ -3950,9 +4360,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.8", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -3961,8 +4381,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.8", + "spki 0.7.3", ] [[package]] @@ -4010,6 +4430,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "poly1305" version = "0.8.0" @@ -4061,7 +4487,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", ] [[package]] @@ -4189,7 +4615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring 0.16.20", "rustc-hash", "rustls", @@ -4221,6 +4647,25 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -4228,10 +4673,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4247,6 +4702,9 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] [[package]] name = "rand_core" @@ -4254,7 +4712,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -4293,7 +4760,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom", + "getrandom 0.2.12", "libredox", "thiserror", ] @@ -4348,7 +4815,7 @@ version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -4408,13 +4875,24 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac 0.12.1", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -4440,7 +4918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "getrandom", + "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -4469,10 +4947,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -4553,7 +5031,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -4650,16 +5128,30 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.8", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -4680,7 +5172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "901f761681f97db3db836ef9e094acdd8756c40215326c194201941947164ef1" dependencies = [ "once_cell", - "rand", + "rand 0.8.5", "sentry-types", "serde", "serde_json", @@ -4705,7 +5197,7 @@ checksum = "da956cca56e0101998c8688bc65ce1a96f00673a0e58e663664023d4c7911e82" dependencies = [ "debugid", "hex", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror", @@ -4886,6 +5378,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -4965,6 +5467,19 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] [[package]] name = "spki" @@ -4973,9 +5488,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.8", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -4994,12 +5515,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" - [[package]] name = "strum" version = "0.25.0" @@ -5037,9 +5552,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "symlink" @@ -5108,6 +5623,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.10.0" @@ -5201,10 +5722,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" dependencies = [ "anyhow", - "hmac", + "hmac 0.12.1", "once_cell", "pbkdf2", - "rand", + "rand 0.8.5", "rustc-hash", "sha2 0.10.8", "thiserror", @@ -5506,6 +6027,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "treediff" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e8d5ad7ce14bb82b7e61ccc0ca961005a275a060b9644a2431aa11553c2ff" +dependencies = [ + "serde_json", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5518,55 +6048,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucan" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3722c8cba706d28123758300ca0738852b5132b37a7c656f59b9484ac8f2435" -dependencies = [ - "anyhow", - "async-recursion", - "async-trait", - "base64", - "bs58", - "cid", - "futures", - "getrandom", - "instant", - "libipld-core", - "libipld-json", - "log", - "rand", - "serde", - "serde_json", - "strum 0.24.1", - "strum_macros", - "unsigned-varint 0.7.2", - "url", -] - -[[package]] -name = "ucan-key-support" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "204146152a164519150c4e9c27f9a728c04d50b7f8a4668c33e5f84d2e536226" -dependencies = [ - "anyhow", - "async-trait", - "bs58", - "ed25519-zebra", - "js-sys", - "log", - "npm_rs", - "p256", - "rsa", - "sha2 0.10.8", - "ucan", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "uint" version = "0.9.5" @@ -5784,6 +6265,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -6156,7 +6643,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775ccf0bffa4cae6de1c534ffda97b032d7144d648bcc07ad56f73ac4b064b6b" dependencies = [ "getopts", - "rand", + "rand 0.8.5", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.5.1", + "zeroize", ] [[package]] @@ -6214,7 +6721,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.1", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -6230,7 +6737,7 @@ dependencies = [ "nohash-hasher", "parking_lot 0.12.1", "pin-project", - "rand", + "rand 0.8.5", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index fe5bd670a..6c0a65974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,29 +10,46 @@ members = [ "rust/noosphere-ipfs", "rust/noosphere-ns", "rust/noosphere-storage", + "rust/noosphere-ucan", + "rust/noosphere-ucan-key-support", ] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" [workspace.dependencies] +noosphere = { path = "rust/noosphere" } +noosphere-cli = { path = "rust/noosphere-cli" } +noosphere-collections = { path = "rust/noosphere-collections" } +noosphere-common = { path = "rust/noosphere-common" } +noosphere-core = { path = "rust/noosphere-core" } +noosphere-gateway = { path = "rust/noosphere-gateway" } +noosphere-into = { path = "rust/noosphere-into" } +noosphere-ipfs = { path = "rust/noosphere-ipfs" } +noosphere-ns = { path = "rust/noosphere-ns" } +noosphere-storage = { path = "rust/noosphere-storage" } +noosphere-ucan = { path = "rust/noosphere-ucan" } +noosphere-ucan-key-support = { path = "rust/noosphere-ucan-key-support" } anyhow = { version = "1" } async-recursion = { version = "1" } async-stream = { version = "0.3" } +async-trait = { version = "0.1" } axum = { version = "^0.7.3" } axum-tracing-opentelemetry = { version = "0.16.0" } axum-extra = { version = "^0.9.1" } base64 = { version = "^0.21" } +bs58 = { version = "^0.5" } byteorder = { version = "~1.5" } # keep in sync with pinned libipld-* crates bytes = { version = "^1" } cid = { version = "0.10" } clap-vergen = { version = "0.2.0" } deterministic-bloom = { version = "0.1.0" } directories = { version = "5" } -ed25519-zebra = { version = "3" } +ed25519-zebra = { version = "4" } fastcdc = { version = "3.1" } -futures = { version = "0.3" } +futures = { version = "^0.3.30" } futures-util = { version = "0.3" } +getrandom = { version = "~0.2", features = ["js"]} gloo-net = { version = "0.4" } gloo-timers = { version = "0.3", features = ["futures"] } ignore = { version = "0.4.20" } @@ -52,6 +69,7 @@ serde = { version = "^1" } serde_json = { version = "^1" } serde_urlencoded = { version = "~0.7" } serde-wasm-bindgen = { version = "0.4" } +serde_ipld_dagcbor = { version = "0.4" } strum = { version = "0.25" } strum_macros = { version = "0.25" } subtext = { version = "0.3.5" } @@ -65,8 +83,7 @@ tower = { version = "^0.4.13" } tower-http = { version = "^0.5" } tracing = { version = "0.1" } tracing-subscriber = { version = "~0.3.18", features = ["env-filter", "tracing-log", "json"] } -ucan = { version = "0.4.0" } -ucan-key-support = { version = "0.1.7" } +unsigned-varint = "0.8" url = { version = "^2" } vergen = { version = "8.2.6", features = ["build", "cargo", "git", "gitcl"] } vergen-pretty = { version = "0.3.1" } diff --git a/release-please-config.json b/release-please-config.json index a77473911..555a9fdef 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -19,6 +19,8 @@ "rust/noosphere-into": {}, "rust/noosphere-ns": {}, "rust/noosphere-storage": {}, + "rust/noosphere-ucan": {}, + "rust/noosphere-ucan-key-support": {}, "rust/noosphere": { "draft": true } diff --git a/rust/noosphere-cli/Cargo.toml b/rust/noosphere-cli/Cargo.toml index 9a2e44f4d..f2adf0577 100644 --- a/rust/noosphere-cli/Cargo.toml +++ b/rust/noosphere-cli/Cargo.toml @@ -9,7 +9,7 @@ categories = [ "filesystem", "command-line interface" ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-cli" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -39,7 +39,7 @@ tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["cors", "trace"] } -async-trait = "~0.1" +async-trait = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, optional = true } iroh-car = { workspace = true } @@ -50,14 +50,14 @@ mime_guess = "^2" witty-phrase-generator = "~0.2" globset = "~0.4" -noosphere-ipfs = { version = "0.8.6", path = "../noosphere-ipfs" } -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-gateway = { version = "0.12.0", path = "../noosphere-gateway" } -noosphere-ns = { version = "0.12.0", path = "../noosphere-ns", optional = true } -noosphere = { version = "0.16.1", path = "../noosphere" } -ucan = { workspace = true } -ucan-key-support = { workspace = true } +noosphere-ipfs = { workspace = true } +noosphere-core = { workspace = true } +noosphere-storage = { workspace = true } +noosphere-gateway = { workspace = true } +noosphere-ns = { workspace = true, optional = true } +noosphere = { workspace = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } vergen-pretty = { workspace = true } diff --git a/rust/noosphere-cli/src/native/commands/key.rs b/rust/noosphere-cli/src/native/commands/key.rs index 05e050129..a5d90891a 100644 --- a/rust/noosphere-cli/src/native/commands/key.rs +++ b/rust/noosphere-cli/src/native/commands/key.rs @@ -1,8 +1,8 @@ //! Concrete implementations of subcommands to manage device keys use anyhow::Result; use noosphere::key::KeyStorage; +use noosphere_ucan::crypto::KeyMaterial; use serde_json::json; -use ucan::crypto::KeyMaterial; use crate::native::workspace::Workspace; diff --git a/rust/noosphere-cli/src/native/commands/sphere/auth.rs b/rust/noosphere-cli/src/native/commands/sphere/auth.rs index 9343fdc82..46e8f18a8 100644 --- a/rust/noosphere-cli/src/native/commands/sphere/auth.rs +++ b/rust/noosphere-cli/src/native/commands/sphere/auth.rs @@ -1,13 +1,12 @@ -use std::{collections::BTreeMap, convert::TryFrom}; - use anyhow::{anyhow, Result}; use cid::Cid; use noosphere_core::context::{ HasSphereContext, SphereAuthorityRead, SphereAuthorityWrite, SphereWalker, }; use noosphere_core::data::{Did, Jwt, Link}; +use noosphere_ucan::{store::UcanJwtStore, Ucan}; use serde_json::{json, Value}; -use ucan::{store::UcanJwtStore, Ucan}; +use std::collections::BTreeMap; use tokio_stream::StreamExt; diff --git a/rust/noosphere-cli/src/native/commands/sphere/history.rs b/rust/noosphere-cli/src/native/commands/sphere/history.rs index c49be54de..fb0d1712c 100644 --- a/rust/noosphere-cli/src/native/commands/sphere/history.rs +++ b/rust/noosphere-cli/src/native/commands/sphere/history.rs @@ -1,8 +1,8 @@ use anyhow::Result; use noosphere_core::context::HasSphereContext; use noosphere_core::data::MapOperation; +use noosphere_ucan::Ucan; use tokio_stream::StreamExt; -use ucan::Ucan; use crate::workspace::Workspace; diff --git a/rust/noosphere-cli/src/native/commands/sphere/mod.rs b/rust/noosphere-cli/src/native/commands/sphere/mod.rs index 423e11515..756a722f9 100644 --- a/rust/noosphere-cli/src/native/commands/sphere/mod.rs +++ b/rust/noosphere-cli/src/native/commands/sphere/mod.rs @@ -33,8 +33,8 @@ use noosphere_core::{ data::Mnemonic, }; +use noosphere_ucan::crypto::KeyMaterial; use tokio::sync::Mutex; -use ucan::crypto::KeyMaterial; use url::Url; /// Create a sphere, assigning authority to modify it to the given key diff --git a/rust/noosphere-cli/src/native/workspace.rs b/rust/noosphere-cli/src/native/workspace.rs index 52626a229..c5e38cd4b 100644 --- a/rust/noosphere-cli/src/native/workspace.rs +++ b/rust/noosphere-cli/src/native/workspace.rs @@ -10,11 +10,11 @@ use noosphere_core::context::{ }; use noosphere_core::data::{Did, Link, LinkRecord, MemoIpld}; use noosphere_storage::{KeyValueStore, SphereDb, StorageConfig}; +use noosphere_ucan::crypto::KeyMaterial; use serde_json::Value; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::io::AsyncReadExt; -use ucan::crypto::KeyMaterial; use url::Url; use noosphere::key::InsecureKeyStorage; diff --git a/rust/noosphere-collections/Cargo.toml b/rust/noosphere-collections/Cargo.toml index 2f2fd5b3e..9cd7f8293 100644 --- a/rust/noosphere-collections/Cargo.toml +++ b/rust/noosphere-collections/Cargo.toml @@ -9,7 +9,7 @@ categories = [ "asynchronous", "web-assembly" ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-collections" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -27,15 +27,15 @@ byteorder = { workspace = true } async-recursion = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } +noosphere-storage = { workspace = true } tokio = { workspace = true, features = ["sync", "io-util"] } tokio-stream = { workspace = true } async-stream = { workspace = true } [dev-dependencies] -unsigned-varint = "0.8" -serde_ipld_dagcbor = "0.4" +unsigned-varint = { workspace = true } +serde_ipld_dagcbor = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/rust/noosphere-collections/src/hamt/bitfield.rs b/rust/noosphere-collections/src/hamt/bitfield.rs index 450c18cfa..3336bfbde 100644 --- a/rust/noosphere-collections/src/hamt/bitfield.rs +++ b/rust/noosphere-collections/src/hamt/bitfield.rs @@ -8,7 +8,6 @@ use std::u64; use byteorder::{BigEndian, ByteOrder}; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_bytes; #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct Bitfield([u64; 4]); diff --git a/rust/noosphere-collections/src/hamt/pointer.rs b/rust/noosphere-collections/src/hamt/pointer.rs index b160c2cd5..e86dfb08c 100644 --- a/rust/noosphere-collections/src/hamt/pointer.rs +++ b/rust/noosphere-collections/src/hamt/pointer.rs @@ -5,7 +5,6 @@ use anyhow::{anyhow, Result}; use std::cmp::Ordering; -use std::convert::{TryFrom, TryInto}; use cid::Cid; use libipld_core::ipld::Ipld; diff --git a/rust/noosphere-common/Cargo.toml b/rust/noosphere-common/Cargo.toml index 7b3e4a74e..6d034236c 100644 --- a/rust/noosphere-common/Cargo.toml +++ b/rust/noosphere-common/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" description = "Common, generic utilities that are shared across other Noosphere packages" keywords = ["noosphere"] categories = [] -rust-version = "1.70.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-common" repository = "https://github.com/subconsciousnetwork/noosphere" diff --git a/rust/noosphere-common/src/channel.rs b/rust/noosphere-common/src/channel.rs index 7920ceaa7..16b77c24b 100644 --- a/rust/noosphere-common/src/channel.rs +++ b/rust/noosphere-common/src/channel.rs @@ -2,8 +2,7 @@ //! producers to send messages to a single subscriber, with each message //! able to be responded to by the subscriber. -use core::{fmt, result::Result}; -use tokio; +use core::fmt; use tokio::sync::{mpsc, mpsc::error::SendError, oneshot, oneshot::error::RecvError}; impl std::error::Error for ChannelError {} diff --git a/rust/noosphere-core/Cargo.toml b/rust/noosphere-core/Cargo.toml index 6c16e1b95..a55313a19 100644 --- a/rust/noosphere-core/Cargo.toml +++ b/rust/noosphere-core/Cargo.toml @@ -11,7 +11,7 @@ categories = [ "web-programming", "web-assembly" ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-core" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -27,7 +27,7 @@ helpers = [] tracing = { workspace = true } cid = { workspace = true } url = { workspace = true, features = ["serde"] } -async-trait = "~0.1" +async-trait = { workspace = true } async-recursion = { workspace = true } async-stream = { workspace = true } # NOTE: async-once-cell 0.4.0 shipped unstable feature usage @@ -57,18 +57,17 @@ reqwest = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } tokio-util = { workspace = true, features = ["io"] } -noosphere-common = { version = "0.1.2", path = "../noosphere-common" } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-collections = { version = "0.7.1", path = "../noosphere-collections" } - -ucan = { workspace = true } -ucan-key-support = { workspace = true } +noosphere-common = { workspace = true } +noosphere-storage = { workspace = true } +noosphere-collections = { workspace = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } sentry-tracing = { workspace = true, optional = true } [dev-dependencies] wasm-bindgen-test = { workspace = true } serde_bytes = "~0.11" -noosphere-common = { version = "0.1.2", path = "../noosphere-common", features = ["helpers"] } +noosphere-common = { workspace = true, features = ["helpers"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true, features = ["full"] } @@ -76,7 +75,7 @@ tracing-subscriber = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] # NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "~0.2", features = ["js"] } +getrandom = { workspace = true, features = ["js"] } gloo-net = { workspace = true } wasm-streams = { workspace = true } wasm-bindgen = { workspace = true } diff --git a/rust/noosphere-core/src/api/client.rs b/rust/noosphere-core/src/api/client.rs index c74cdaddd..037cee19c 100644 --- a/rust/noosphere-core/src/api/client.rs +++ b/rust/noosphere-core/src/api/client.rs @@ -18,16 +18,16 @@ use crate::{ data::{Link, MemoIpld}, }; use noosphere_storage::{block_deserialize, block_serialize, BlockStore}; -use reqwest::header::HeaderMap; -use tokio_stream::{Stream, StreamExt}; -use tokio_util::io::StreamReader; -use ucan::{ +use noosphere_ucan::{ builder::UcanBuilder, capability::CapabilityView, crypto::{did::DidParser, KeyMaterial}, store::{UcanJwtStore, UcanStore}, ucan::Ucan, }; +use reqwest::header::HeaderMap; +use tokio_stream::{Stream, StreamExt}; +use tokio_util::io::StreamReader; use url::Url; #[cfg(doc)] diff --git a/rust/noosphere-core/src/api/headers/ucan.rs b/rust/noosphere-core/src/api/headers/ucan.rs index 57d6224bc..6a8cc0655 100644 --- a/rust/noosphere-core/src/api/headers/ucan.rs +++ b/rust/noosphere-core/src/api/headers/ucan.rs @@ -2,8 +2,8 @@ use crate::{authority::SUPPORTED_KEYS, data::Jwt}; use anyhow::anyhow; use axum_extra::headers::{self, Header, HeaderName, HeaderValue}; use cid::Cid; +use noosphere_ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore}; use once_cell::sync::Lazy; -use ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore}; static UCAN_NAME: Lazy = Lazy::new(|| HeaderName::from_static("ucan")); diff --git a/rust/noosphere-core/src/api/v0alpha1/data.rs b/rust/noosphere-core/src/api/v0alpha1/data.rs index c170750a0..dd4d979f5 100644 --- a/rust/noosphere-core/src/api/v0alpha1/data.rs +++ b/rust/noosphere-core/src/api/v0alpha1/data.rs @@ -12,14 +12,14 @@ use crate::{ use anyhow::{anyhow, Result}; use cid::Cid; use noosphere_storage::{base64_decode, base64_encode}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use ucan::{ +use noosphere_ucan::{ chain::ProofChain, crypto::{did::DidParser, KeyMaterial}, store::UcanStore, Ucan, }; +use serde::{Deserialize, Serialize}; +use thiserror::Error; /// The query parameters expected for the "replicate" API route. #[derive(Debug, Serialize, Deserialize)] diff --git a/rust/noosphere-core/src/authority/author.rs b/rust/noosphere-core/src/authority/author.rs index c307caa3c..ada38efc8 100644 --- a/rust/noosphere-core/src/authority/author.rs +++ b/rust/noosphere-core/src/authority/author.rs @@ -7,11 +7,11 @@ use crate::{ }; use anyhow::{anyhow, Result}; use noosphere_storage::{SphereDb, Storage}; -use ucan::{ +use noosphere_ucan::{ chain::ProofChain, crypto::{did::DidParser, KeyMaterial}, }; -use ucan_key_support::ed25519::Ed25519KeyMaterial; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; use super::generate_capability; @@ -159,7 +159,7 @@ where mod tests { use anyhow::Result; use noosphere_storage::{MemoryStorage, SphereDb}; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; use crate::{ authority::{generate_capability, generate_ed25519_key, Authorization, SphereAbility}, diff --git a/rust/noosphere-core/src/authority/authorization.rs b/rust/noosphere-core/src/authority/authorization.rs index 2cbbf1f05..2d9664323 100644 --- a/rust/noosphere-core/src/authority/authorization.rs +++ b/rust/noosphere-core/src/authority/authorization.rs @@ -1,13 +1,12 @@ -use std::{convert::TryFrom, fmt::Display, str::FromStr}; - use anyhow::{anyhow, Result}; use cid::Cid; use libipld_core::{ipld::Ipld, raw::RawCodec}; use noosphere_storage::block_encode; -use ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan}; +use noosphere_ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan}; +use std::{fmt::Display, str::FromStr}; #[cfg(doc)] -use ucan::ipld::UcanIpld; +use noosphere_ucan::ipld::UcanIpld; #[cfg(doc)] use crate::data::Jwt; diff --git a/rust/noosphere-core/src/authority/capability.rs b/rust/noosphere-core/src/authority/capability.rs index 71624d892..4d06ef8e3 100644 --- a/rust/noosphere-core/src/authority/capability.rs +++ b/rust/noosphere-core/src/authority/capability.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Result}; -use serde_json::json; -use ucan::capability::{ +use noosphere_ucan::capability::{ Ability, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, }; +use serde_json::json; +use std::fmt; use url::Url; /// The ordinal levels of authority allowed within Noosphere @@ -20,15 +21,18 @@ pub enum SphereAbility { impl Ability for SphereAbility {} -impl ToString for SphereAbility { - fn to_string(&self) -> String { - match self { - SphereAbility::Authorize => "sphere/authorize", - SphereAbility::Publish => "sphere/publish", - SphereAbility::Push => "sphere/push", - SphereAbility::Fetch => "sphere/fetch", - } - .into() +impl fmt::Display for SphereAbility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + SphereAbility::Authorize => "sphere/authorize", + SphereAbility::Publish => "sphere/publish", + SphereAbility::Push => "sphere/push", + SphereAbility::Fetch => "sphere/fetch", + } + ) } } @@ -62,9 +66,9 @@ impl Scope for SphereReference { } } -impl ToString for SphereReference { - fn to_string(&self) -> String { - format!("sphere:{}", self.did) +impl fmt::Display for SphereReference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "sphere:{}", self.did) } } @@ -96,7 +100,7 @@ pub const SPHERE_SEMANTICS: SphereSemantics = SphereSemantics {}; /// /// ``` /// use noosphere_core::{authority::{generate_capability, SphereAbility, SphereReference}}; -/// use ucan::capability::{CapabilityView, ResourceUri, Resource}; +/// use noosphere_ucan::capability::{CapabilityView, ResourceUri, Resource}; /// use serde_json::json; /// /// let identity = "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP"; diff --git a/rust/noosphere-core/src/authority/key_material.rs b/rust/noosphere-core/src/authority/key_material.rs index e79b68eef..df0e908a9 100644 --- a/rust/noosphere-core/src/authority/key_material.rs +++ b/rust/noosphere-core/src/authority/key_material.rs @@ -2,8 +2,8 @@ use crate::data::Mnemonic; use anyhow::{anyhow, Result}; use bip39::{Language, Mnemonic as BipMnemonic}; use ed25519_zebra::{SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey}; -use ucan::crypto::did::KeyConstructorSlice; -use ucan_key_support::{ +use noosphere_ucan::crypto::did::KeyConstructorSlice; +use noosphere_ucan_key_support::{ ed25519::{bytes_to_ed25519_key, Ed25519KeyMaterial, ED25519_MAGIC_BYTES}, rsa::{bytes_to_rsa_key, RSA_MAGIC_BYTES}, }; diff --git a/rust/noosphere-core/src/authority/walk.rs b/rust/noosphere-core/src/authority/walk.rs index 6cb0b8caf..aa1915e97 100644 --- a/rust/noosphere-core/src/authority/walk.rs +++ b/rust/noosphere-core/src/authority/walk.rs @@ -1,6 +1,6 @@ use anyhow::Result; use cid::Cid; -use ucan::{store::UcanJwtStore, Ucan}; +use noosphere_ucan::{store::UcanJwtStore, Ucan}; /// Walk a [Ucan] and collect all of the supporting proofs that /// verify the link publisher's authority to publish the link diff --git a/rust/noosphere-core/src/context/authority/read.rs b/rust/noosphere-core/src/context/authority/read.rs index 03c487461..5f40146f4 100644 --- a/rust/noosphere-core/src/context/authority/read.rs +++ b/rust/noosphere-core/src/context/authority/read.rs @@ -128,7 +128,7 @@ mod tests { use crate::{authority::Access, data::Did}; use anyhow::Result; - use ucan::crypto::KeyMaterial; + use noosphere_ucan::crypto::KeyMaterial; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/context/authority/write.rs b/rust/noosphere-core/src/context/authority/write.rs index f6bf1582a..6b21a5892 100644 --- a/rust/noosphere-core/src/context/authority/write.rs +++ b/rust/noosphere-core/src/context/authority/write.rs @@ -10,8 +10,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use cid::Cid; use noosphere_storage::{Storage, UcanStore}; +use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use tokio_stream::StreamExt; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; /// Any type which implements [SphereAuthorityWrite] is able to manipulate the /// [AuthorityIpld] section of a sphere. This includes authorizing other keys @@ -230,8 +230,8 @@ mod tests { use crate::data::Did; use anyhow::Result; + use noosphere_ucan::crypto::KeyMaterial; use tokio::sync::Mutex; - use ucan::crypto::KeyMaterial; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/context/context.rs b/rust/noosphere-core/src/context/context.rs index 98c25d4ec..aad82397d 100644 --- a/rust/noosphere-core/src/context/context.rs +++ b/rust/noosphere-core/src/context/context.rs @@ -10,8 +10,8 @@ use crate::{ view::{Sphere, SphereMutation}, }; use noosphere_storage::{KeyValueStore, SphereDb, Storage}; +use noosphere_ucan::crypto::{did::DidParser, KeyMaterial}; use tokio::sync::OnceCell; -use ucan::crypto::{did::DidParser, KeyMaterial}; use url::Url; #[cfg(doc)] @@ -266,8 +266,8 @@ mod tests { }; use noosphere_storage::{MemoryStorage, SphereDb, Storage}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; use serde_json::json; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/context/petname/write.rs b/rust/noosphere-core/src/context/petname/write.rs index a782a936c..dc76e9c42 100644 --- a/rust/noosphere-core/src/context/petname/write.rs +++ b/rust/noosphere-core/src/context/petname/write.rs @@ -2,7 +2,7 @@ use crate::data::{Did, IdentityIpld, LinkRecord}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use noosphere_storage::Storage; -use ucan::store::UcanJwtStore; +use noosphere_ucan::store::UcanJwtStore; use crate::context::{internal::SphereContextInternal, HasMutableSphereContext, SpherePetnameRead}; diff --git a/rust/noosphere-core/src/context/replication/write.rs b/rust/noosphere-core/src/context/replication/write.rs index ea7626c42..d95a7dba4 100644 --- a/rust/noosphere-core/src/context/replication/write.rs +++ b/rust/noosphere-core/src/context/replication/write.rs @@ -3,7 +3,7 @@ use instant::Duration; use async_trait::async_trait; use noosphere_storage::Storage; -use ucan::{builder::UcanBuilder, store::UcanJwtStore}; +use noosphere_ucan::{builder::UcanBuilder, store::UcanJwtStore}; use crate::{ authority::{generate_capability, SphereAbility}, diff --git a/rust/noosphere-core/src/data/address.rs b/rust/noosphere-core/src/data/address.rs index e833886bd..1fcf70cf9 100644 --- a/rust/noosphere-core/src/data/address.rs +++ b/rust/noosphere-core/src/data/address.rs @@ -5,10 +5,10 @@ use anyhow::Result; use cid::Cid; use libipld_cbor::DagCborCodec; use noosphere_storage::BlockStore; +use noosphere_ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan}; use serde::{de, ser, Deserialize, Serialize}; use std::fmt::Debug; -use std::{convert::TryFrom, fmt::Display, ops::Deref, str::FromStr}; -use ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan}; +use std::{fmt::Display, ops::Deref, str::FromStr}; use super::{Did, IdentitiesIpld, Jwt, Link, MemoIpld}; @@ -354,7 +354,7 @@ mod tests { view::{Sphere, SPHERE_LIFETIME}, }; use noosphere_storage::{MemoryStorage, SphereDb, UcanStore}; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; @@ -468,7 +468,7 @@ mod tests { )) .with_fact(LINK_RECORD_FACT_NAME, cid_link.to_string()) .witnessed_by(&delegate_ucan, None) - .with_expiration(ucan::time::now() - 1234) + .with_expiration(noosphere_ucan::time::now() - 1234) .build()? .sign() .await? diff --git a/rust/noosphere-core/src/data/authority.rs b/rust/noosphere-core/src/data/authority.rs index 1a911f292..50eda1dcc 100644 --- a/rust/noosphere-core/src/data/authority.rs +++ b/rust/noosphere-core/src/data/authority.rs @@ -1,8 +1,8 @@ use anyhow::Result; use cid::Cid; use libipld_cbor::DagCborCodec; +use noosphere_ucan::{crypto::KeyMaterial, store::UcanJwtStore, Ucan}; use std::{hash::Hash, str::FromStr}; -use ucan::{crypto::KeyMaterial, store::UcanJwtStore, Ucan}; use noosphere_storage::{base64_decode, base64_encode, BlockStore, UcanStore}; use serde::{Deserialize, Serialize}; @@ -133,7 +133,7 @@ impl RevocationIpld { #[cfg(test)] mod tests { use noosphere_storage::{MemoryStore, UcanStore}; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; use crate::authority::generate_ed25519_key; diff --git a/rust/noosphere-core/src/data/bundle.rs b/rust/noosphere-core/src/data/bundle.rs index 0313012f9..4de5a9dd6 100644 --- a/rust/noosphere-core/src/data/bundle.rs +++ b/rust/noosphere-core/src/data/bundle.rs @@ -11,8 +11,8 @@ use futures::{pin_mut, StreamExt}; use libipld_cbor::DagCborCodec; use libipld_core::raw::RawCodec; use noosphere_storage::{block_deserialize, block_serialize, BlockStore, UcanStore}; +use noosphere_ucan::{store::UcanJwtStore, Ucan}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use ucan::{store::UcanJwtStore, Ucan}; use crate::{ data::{ @@ -477,7 +477,7 @@ mod tests { use libipld_cbor::DagCborCodec; use libipld_core::{ipld::Ipld, raw::RawCodec}; use noosphere_storage::{block_serialize, BlockStore, MemoryStore, UcanStore}; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/data/changelog.rs b/rust/noosphere-core/src/data/changelog.rs index 491e5f4e3..8466f330e 100644 --- a/rust/noosphere-core/src/data/changelog.rs +++ b/rust/noosphere-core/src/data/changelog.rs @@ -1,6 +1,5 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; -use std::default::Default; #[cfg(doc)] use crate::data::{Did, VersionedMapIpld}; diff --git a/rust/noosphere-core/src/data/memo.rs b/rust/noosphere-core/src/data/memo.rs index 1ce361e9b..0faa03c80 100644 --- a/rust/noosphere-core/src/data/memo.rs +++ b/rust/noosphere-core/src/data/memo.rs @@ -7,8 +7,8 @@ use anyhow::{anyhow, Result}; use cid::Cid; use libipld_cbor::DagCborCodec; use noosphere_common::ConditionalSend; +use noosphere_ucan::{crypto::KeyMaterial, Ucan}; use serde::{Deserialize, Serialize}; -use ucan::{crypto::KeyMaterial, Ucan}; use crate::data::Header; diff --git a/rust/noosphere-core/src/data/sphere.rs b/rust/noosphere-core/src/data/sphere.rs index df89c0f6d..07bd673e2 100644 --- a/rust/noosphere-core/src/data/sphere.rs +++ b/rust/noosphere-core/src/data/sphere.rs @@ -60,7 +60,7 @@ impl SphereIpld { mod tests { use anyhow::Result; use libipld_cbor::DagCborCodec; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/data/strings.rs b/rust/noosphere-core/src/data/strings.rs index c97db9c63..519c9a5fb 100644 --- a/rust/noosphere-core/src/data/strings.rs +++ b/rust/noosphere-core/src/data/strings.rs @@ -1,8 +1,8 @@ use anyhow::Result; use noosphere_collections::hamt::Hash as HamtHash; +use noosphere_ucan::crypto::{did::DidParser, KeyMaterial}; use serde::{Deserialize, Serialize}; use std::{fmt::Display, hash::Hash, ops::Deref, sync::Arc}; -use ucan::crypto::{did::DidParser, KeyMaterial}; use crate::authority::{restore_ed25519_key, SUPPORTED_KEYS}; diff --git a/rust/noosphere-core/src/helpers/context.rs b/rust/noosphere-core/src/helpers/context.rs index 84033ce55..3f95d8bfb 100644 --- a/rust/noosphere-core/src/helpers/context.rs +++ b/rust/noosphere-core/src/helpers/context.rs @@ -11,8 +11,8 @@ use crate::{ }; use anyhow::Result; use noosphere_storage::{BlockStore, MemoryStorage, SphereDb, Storage, TrackingStorage, UcanStore}; +use noosphere_ucan::crypto::KeyMaterial; use tokio::{io::AsyncReadExt, sync::Mutex}; -use ucan::crypto::KeyMaterial; use crate::{ context::{ diff --git a/rust/noosphere-core/src/helpers/link.rs b/rust/noosphere-core/src/helpers/link.rs index f8290f4e4..0a77ebbb7 100644 --- a/rust/noosphere-core/src/helpers/link.rs +++ b/rust/noosphere-core/src/helpers/link.rs @@ -1,6 +1,6 @@ use anyhow::Result; use noosphere_storage::{MemoryStorage, SphereDb}; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; +use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; use crate::{ authority::{generate_capability, generate_ed25519_key, SphereAbility}, diff --git a/rust/noosphere-core/src/stream/memo.rs b/rust/noosphere-core/src/stream/memo.rs index c2cee53e6..97781f4ce 100644 --- a/rust/noosphere-core/src/stream/memo.rs +++ b/rust/noosphere-core/src/stream/memo.rs @@ -12,9 +12,9 @@ use cid::Cid; use libipld_cbor::DagCborCodec; use noosphere_common::{spawn, ConditionalSend, TaskQueue}; use noosphere_storage::{BlockStore, BlockStoreTap, UcanStore}; +use noosphere_ucan::{store::UcanJwtStore, Ucan}; use tokio::select; use tokio_stream::{Stream, StreamExt}; -use ucan::{store::UcanJwtStore, Ucan}; use crate::stream::walk::{ walk_versioned_map_changes_and, walk_versioned_map_elements, walk_versioned_map_elements_and, diff --git a/rust/noosphere-core/src/stream/mod.rs b/rust/noosphere-core/src/stream/mod.rs index 350d1df99..6012b9164 100644 --- a/rust/noosphere-core/src/stream/mod.rs +++ b/rust/noosphere-core/src/stream/mod.rs @@ -18,8 +18,8 @@ mod tests { use anyhow::Result; use cid::Cid; use libipld_core::{codec::Codec, ipld::Ipld, raw::RawCodec}; + use noosphere_ucan::{crypto::KeyMaterial, store::UcanJwtStore}; use std::collections::BTreeSet; - use ucan::{crypto::KeyMaterial, store::UcanJwtStore}; use crate::{ authority::{generate_ed25519_key, Access}, diff --git a/rust/noosphere-core/src/stream/walk.rs b/rust/noosphere-core/src/stream/walk.rs index b205b7717..2140120b5 100644 --- a/rust/noosphere-core/src/stream/walk.rs +++ b/rust/noosphere-core/src/stream/walk.rs @@ -4,7 +4,6 @@ use crate::{ }; use anyhow::Result; use noosphere_storage::BlockStore; -use std::ops::Fn; use tokio_stream::StreamExt; /// Given a [VersionedMap], visit its changelog and all of its underlying entries diff --git a/rust/noosphere-core/src/tracing.rs b/rust/noosphere-core/src/tracing.rs index a382676dc..6aa06598b 100644 --- a/rust/noosphere-core/src/tracing.rs +++ b/rust/noosphere-core/src/tracing.rs @@ -18,6 +18,8 @@ pub static NOOSPHERE_LOG_LEVEL_CRATES: &[&str] = &[ "noosphere_car", "noosphere_api", "noosphere_ns", + "noosphere_ucan", + "noosphere_ucan_key_support", "orb", "orb_ns", "tower_http", @@ -84,11 +86,12 @@ impl From for NoosphereLogLevel { /// The format used to display logs. The amount of minutia and noise in the format /// increases in the order of the variants from top to bottom. -#[derive(Clone, Display, EnumString)] +#[derive(Default, Clone, Display, EnumString)] pub enum NoosphereLogFormat { /// As the name implies, this is the most minimal format. `INFO` events only /// display the contents of the log line. Other events are prefixed with /// their event name. + #[default] #[strum(serialize = "minimal")] Minimal, /// Verbose formatting that includes minutia such as timestamps and code @@ -105,17 +108,11 @@ pub enum NoosphereLogFormat { Structured, } -impl Default for NoosphereLogFormat { - fn default() -> Self { - NoosphereLogFormat::Minimal - } -} - /// The filter level for the Noosphere-centric crates listed in /// [NOOSPHERE_LOG_LEVEL_CRATES]. These filter levels correspond 1:1 with those /// described in /// [`env-filter`](https://docs.rs/env_logger/0.10.0/env_logger/#enabling-logging) -#[derive(Clone, Display, EnumString)] +#[derive(Default, Clone, Display, EnumString)] pub enum NoosphereLogLevel { /// Equivalent to [tracing::Level::TRACE] #[strum(serialize = "trace")] @@ -124,6 +121,7 @@ pub enum NoosphereLogLevel { #[strum(serialize = "debug")] Debug, /// Equivalent to [tracing::Level::INFO] + #[default] #[strum(serialize = "info")] Info, /// Equivalent to [tracing::Level::WARN] @@ -137,12 +135,6 @@ pub enum NoosphereLogLevel { Off, } -impl Default for NoosphereLogLevel { - fn default() -> Self { - NoosphereLogLevel::Info - } -} - #[cfg(not(target_arch = "wasm32"))] impl From for Vec { fn from(noosphere_log_level: NoosphereLogLevel) -> Self { diff --git a/rust/noosphere-core/src/view/mutation.rs b/rust/noosphere-core/src/view/mutation.rs index 38c67c6e8..8b84f58c8 100644 --- a/rust/noosphere-core/src/view/mutation.rs +++ b/rust/noosphere-core/src/view/mutation.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use libipld_cbor::DagCborCodec; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; +use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use crate::{ authority::{generate_capability, Authorization, SphereAbility}, diff --git a/rust/noosphere-core/src/view/sphere.rs b/rust/noosphere-core/src/view/sphere.rs index b7e02b7b8..a53dabaeb 100644 --- a/rust/noosphere-core/src/view/sphere.rs +++ b/rust/noosphere-core/src/view/sphere.rs @@ -6,7 +6,7 @@ use libipld_cbor::DagCborCodec; use tokio::sync::OnceCell; use tokio_stream::StreamExt; -use ucan::{ +use noosphere_ucan::{ builder::UcanBuilder, chain::ProofChain, crypto::{did::DidParser, KeyMaterial}, @@ -1067,8 +1067,8 @@ mod tests { use anyhow::Result; use cid::Cid; use libipld_cbor::DagCborCodec; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use tokio_stream::StreamExt; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-core/src/view/timeline.rs b/rust/noosphere-core/src/view/timeline.rs index e5cc6d592..0330766ed 100644 --- a/rust/noosphere-core/src/view/timeline.rs +++ b/rust/noosphere-core/src/view/timeline.rs @@ -154,7 +154,7 @@ mod tests { use anyhow::Result; use libipld_cbor::DagCborCodec; use noosphere_storage::{BlockStore, MemoryStore}; - use ucan::crypto::KeyMaterial; + use noosphere_ucan::crypto::KeyMaterial; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere-gateway/Cargo.toml b/rust/noosphere-gateway/Cargo.toml index d689e5650..401f80012 100644 --- a/rust/noosphere-gateway/Cargo.toml +++ b/rust/noosphere-gateway/Cargo.toml @@ -8,7 +8,7 @@ categories = [ "network-programming", "asynchronous", ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-gateway" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -25,8 +25,8 @@ tracing = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] reqwest = { workspace = true } -noosphere-common = { version = "0.1.2", path = "../noosphere-common", features = ["helpers"] } -noosphere-core = { version = "0.18.1", path = "../noosphere-core", features = ["helpers"] } +noosphere-common = { workspace = true, features = ["helpers"] } +noosphere-core = { workspace = true, features = ["helpers"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] anyhow = { workspace = true } @@ -43,19 +43,19 @@ tokio-stream = { workspace = true } tokio-util = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["cors", "trace"] } -async-trait = "~0.1" +async-trait = { workspace = true } async-stream = { workspace = true } url = { workspace = true, features = ["serde"] } mime_guess = "^2" -noosphere-ipfs = { version = "0.8.6", path = "../noosphere-ipfs" } -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } -noosphere-ns = { version = "0.12.0", path = "../noosphere-ns" } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-common = { version = "0.1.2", path = "../noosphere-common" } -ucan = { workspace = true } -ucan-key-support = { workspace = true } +noosphere-ipfs = { workspace = true } +noosphere-core = { workspace = true } +noosphere-ns = { workspace = true } +noosphere-storage = { workspace = true } +noosphere-common = { workspace = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } cid = { workspace = true } serde = { workspace = true } diff --git a/rust/noosphere-gateway/src/extractors/authority.rs b/rust/noosphere-gateway/src/extractors/authority.rs index de76fceff..e61526964 100644 --- a/rust/noosphere-gateway/src/extractors/authority.rs +++ b/rust/noosphere-gateway/src/extractors/authority.rs @@ -15,7 +15,7 @@ use noosphere_core::{ data::Did, }; use noosphere_storage::Storage; -use ucan::capability::CapabilityView; +use noosphere_ucan::capability::CapabilityView; use crate::extractors::map_bad_request; diff --git a/rust/noosphere-gateway/src/handlers/v0alpha1/replicate.rs b/rust/noosphere-gateway/src/handlers/v0alpha1/replicate.rs index ccc86e383..ee18e985b 100644 --- a/rust/noosphere-gateway/src/handlers/v0alpha1/replicate.rs +++ b/rust/noosphere-gateway/src/handlers/v0alpha1/replicate.rs @@ -194,8 +194,8 @@ mod tests { }, data::{DelegationIpld, RevocationIpld}, }; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use tokio::sync::Mutex; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; #[tokio::test] async fn it_only_allows_incremental_replication_of_causally_ordered_revisions() -> Result<()> { diff --git a/rust/noosphere-gateway/src/worker/name_system.rs b/rust/noosphere-gateway/src/worker/name_system.rs index ce0583889..058154a4a 100644 --- a/rust/noosphere-gateway/src/worker/name_system.rs +++ b/rust/noosphere-gateway/src/worker/name_system.rs @@ -16,7 +16,6 @@ use std::{ collections::{BTreeMap, BTreeSet}, fmt::Display, future::Future, - string::ToString, sync::Arc, time::Duration, }; @@ -550,7 +549,7 @@ mod tests { helpers::simulated_sphere_context, }; use noosphere_ns::helpers::KeyValueNameResolver; - use ucan::builder::UcanBuilder; + use noosphere_ucan::builder::UcanBuilder; use super::*; @@ -585,7 +584,7 @@ mod tests { .issued_by(&context.author().key) .for_audience(identity) .claiming_capability(&generate_capability(identity, SphereAbility::Publish)) - .with_expiration(ucan::time::now() - 1000) + .with_expiration(noosphere_ucan::time::now() - 1000) .with_fact( LINK_RECORD_FACT_NAME, "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i".to_owned(), diff --git a/rust/noosphere-into/Cargo.toml b/rust/noosphere-into/Cargo.toml index 598df8082..c442022ad 100644 --- a/rust/noosphere-into/Cargo.toml +++ b/rust/noosphere-into/Cargo.toml @@ -9,7 +9,7 @@ categories = [ "web-programming", "file-system" ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-into" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -17,10 +17,12 @@ homepage = "https://github.com/subconsciousnetwork/noosphere" readme = "README.md" [dependencies] -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } +noosphere-core = { workspace = true } +noosphere-storage = { workspace = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } subtext = { workspace = true, features = ["stream"] } -async-trait = "~0.1" +async-trait = { workspace = true } url = { workspace = true } tracing = { workspace = true } anyhow = { workspace = true } @@ -38,11 +40,8 @@ async-stream = { workspace = true } futures = { workspace = true } async-utf8-decoder = { version = "~0.3" } -ucan = { workspace = true } -ucan-key-support = { workspace = true } - [dev-dependencies] -noosphere-core = { version = "0.18.1", path = "../noosphere-core", features = ["helpers"] } +noosphere-core = { workspace = true, features = ["helpers"] } wasm-bindgen-test = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/rust/noosphere-into/examples/notes-to-html/implementation.rs b/rust/noosphere-into/examples/notes-to-html/implementation.rs index 8c26f4409..3714843e4 100644 --- a/rust/noosphere-into/examples/notes-to-html/implementation.rs +++ b/rust/noosphere-into/examples/notes-to-html/implementation.rs @@ -23,7 +23,7 @@ use noosphere_core::{ view::Sphere, }; use noosphere_storage::{MemoryStorage, SphereDb}; -use ucan::crypto::KeyMaterial; +use noosphere_ucan::crypto::KeyMaterial; pub async fn main() -> Result<()> { let storage_provider = MemoryStorage::default(); diff --git a/rust/noosphere-ipfs/Cargo.toml b/rust/noosphere-ipfs/Cargo.toml index ad62e4375..b8eb2e495 100644 --- a/rust/noosphere-ipfs/Cargo.toml +++ b/rust/noosphere-ipfs/Cargo.toml @@ -12,7 +12,7 @@ categories = [ "network-programming", "asynchronous", ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-ipfs" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -21,12 +21,12 @@ readme = "README.md" [features] default = ["storage"] -storage = ["ucan"] +storage = ["noosphere-ucan"] test-kubo = [] [dependencies] anyhow = { workspace = true } -async-trait = "~0.1" +async-trait = { workspace = true } async-stream = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } @@ -39,9 +39,9 @@ tokio-stream = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } url = { workspace = true, features = [ "serde" ] } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-common = { version = "0.1.2", path = "../noosphere-common" } -ucan = { workspace = true, optional = true } +noosphere-storage = { workspace = true } +noosphere-common = { workspace = true } +noosphere-ucan = { workspace = true, optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] hyper = { version = "^0.14.27", features = ["full"] } @@ -54,4 +54,4 @@ iroh-car = { workspace = true } libipld-cbor = { workspace = true } libipld-json = { workspace = true } multihash = { workspace = true } -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } +noosphere-core = { workspace = true } diff --git a/rust/noosphere-ipfs/examples/car.rs b/rust/noosphere-ipfs/examples/car.rs index 6dd9aefbe..d453d6a7e 100644 --- a/rust/noosphere-ipfs/examples/car.rs +++ b/rust/noosphere-ipfs/examples/car.rs @@ -2,21 +2,7 @@ //! CAR-reading facilities in use by Noosphere more generally #[cfg(not(target_arch = "wasm32"))] -use std::env; - -use anyhow::Result; -use cid::Cid; -use iroh_car::CarReader; -use libipld_cbor::DagCborCodec; -use libipld_core::raw::RawCodec; -use multihash::MultihashDigest; - -use noosphere_core::stream::BlockLedger; - -#[cfg(not(target_arch = "wasm32"))] -use tokio::fs::File; - -pub fn hash_for(cid: Cid) -> &'static str { +pub fn hash_for(cid: cid::Cid) -> &'static str { match multihash::Code::try_from(cid.hash().code()) { Ok(multihash::Code::Blake3_256) => "BLAKE3", Ok(multihash::Code::Sha2_256) => "SHA-256", @@ -28,7 +14,10 @@ pub fn hash_for(cid: Cid) -> &'static str { } } -pub fn codec_for(cid: Cid) -> &'static str { +#[cfg(not(target_arch = "wasm32"))] +pub fn codec_for(cid: cid::Cid) -> &'static str { + use libipld_cbor::DagCborCodec; + use libipld_core::raw::RawCodec; match cid.codec() { codec if codec == u64::from(DagCborCodec) => "DAG-CBOR", codec if codec == u64::from(RawCodec) => "Raw", @@ -41,9 +30,17 @@ pub fn main() {} #[cfg(not(target_arch = "wasm32"))] #[cfg_attr(not(target_arch = "wasm32"), tokio::main)] -pub async fn main() -> Result<()> { +pub async fn main() -> anyhow::Result<()> { + use cid::Cid; + use iroh_car::CarReader; + use libipld_cbor::DagCborCodec; use libipld_core::ipld::Ipld; + use libipld_core::raw::RawCodec; + use multihash::MultihashDigest; + use noosphere_core::stream::BlockLedger; use noosphere_storage::block_decode; + use std::env; + use tokio::fs::File; let file = if let Some(arg) = env::args().nth(1) { println!("Opening {arg}...\n"); diff --git a/rust/noosphere-ns/Cargo.toml b/rust/noosphere-ns/Cargo.toml index dec73ce8b..aa7206b8d 100644 --- a/rust/noosphere-ns/Cargo.toml +++ b/rust/noosphere-ns/Cargo.toml @@ -14,7 +14,7 @@ categories = [ "network-programming", "asynchronous", ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-ns" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -31,20 +31,20 @@ lazy_static = "^1" cid = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -futures = "^0.3.30" -async-trait = "~0.1" -ucan = { workspace = true } -ucan-key-support = { workspace = true } +futures = { workspace = true } +async-trait = { workspace = true } tokio = { workspace = true, features = ["io-util", "io-std", "sync", "macros", "rt", "rt-multi-thread"] } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } -noosphere-common = { version = "0.1.2", path = "../noosphere-common" } +noosphere-storage = { workspace = true } +noosphere-core = { workspace = true } +noosphere-common = { workspace = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } libp2p = { version = "0.53.2", default-features = false, features = [ "ed25519", "identify", "dns", "kad", "macros", "noise", "serde", "tcp", "tls", "tokio", "yamux" ] } void = { workspace = true } # noosphere_ns::bin -noosphere = { version = "0.16.1", path = "../noosphere", optional = true } -noosphere-ipfs = { version = "0.8.6", path = "../noosphere-ipfs", optional = true } +noosphere = { workspace = true, optional = true } +noosphere-ipfs = { workspace = true, optional = true } clap = { version = "^4.5", features = ["derive"], optional = true } home = { version = "~0.5", optional = true } toml = { version = "~0.8", optional = true } diff --git a/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs b/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs index 8301b6f7c..9299ece70 100644 --- a/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs +++ b/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs @@ -16,11 +16,11 @@ mod test { use noosphere_core::data::{Did, LinkRecord, LINK_RECORD_FACT_NAME}; use noosphere_core::view::SPHERE_LIFETIME; use noosphere_ns::{Multiaddr, PeerId}; + use noosphere_ucan::builder::UcanBuilder; + use noosphere_ucan::crypto::KeyMaterial; use serde::Deserialize; use tokio; use tokio::sync::oneshot; - use ucan::builder::UcanBuilder; - use ucan::crypto::KeyMaterial; use url::Url; #[derive(Debug, Deserialize)] diff --git a/rust/noosphere-ns/src/bin/orb-ns/runner/config.rs b/rust/noosphere-ns/src/bin/orb-ns/runner/config.rs index f803abcb9..0b91a7272 100644 --- a/rust/noosphere-ns/src/bin/orb-ns/runner/config.rs +++ b/rust/noosphere-ns/src/bin/orb-ns/runner/config.rs @@ -3,8 +3,8 @@ use crate::utils; use anyhow::{anyhow, Result}; use noosphere::key::InsecureKeyStorage; use noosphere_ns::{DhtConfig, Multiaddr, BOOTSTRAP_PEERS}; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; use std::net::SocketAddr; -use ucan_key_support::ed25519::Ed25519KeyMaterial; use url::Url; /// Configuration for [NameSystemRunner], hydrated/resolved from CLI. @@ -98,9 +98,9 @@ impl RunnerNodeConfig { mod tests { use super::*; use noosphere::key::KeyStorage; + use noosphere_ucan::crypto::KeyMaterial; use std::path::PathBuf; use tempfile::TempDir; - use ucan::crypto::KeyMaterial; async fn keys_equal(key_1: &Ed25519KeyMaterial, key_2: &Ed25519KeyMaterial) -> Result { Ok(key_1.get_did().await? == key_2.get_did().await?) diff --git a/rust/noosphere-ns/src/bin/orb-ns/utils.rs b/rust/noosphere-ns/src/bin/orb-ns/utils.rs index 1668ce44e..e2d530c88 100644 --- a/rust/noosphere-ns/src/bin/orb-ns/utils.rs +++ b/rust/noosphere-ns/src/bin/orb-ns/utils.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use noosphere::key::{InsecureKeyStorage, KeyStorage}; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; use std::path::PathBuf; -use ucan_key_support::ed25519::Ed25519KeyMaterial; pub async fn get_key_material( key_storage: &InsecureKeyStorage, diff --git a/rust/noosphere-ns/src/builder.rs b/rust/noosphere-ns/src/builder.rs index dc2a00c1c..819e94a2c 100644 --- a/rust/noosphere-ns/src/builder.rs +++ b/rust/noosphere-ns/src/builder.rs @@ -1,8 +1,8 @@ use crate::{dht::DhtConfig, name_system::NameSystem, DhtClient, NameSystemKeyMaterial}; use anyhow::{anyhow, Result}; -use libp2p::{self, Multiaddr}; +use libp2p::Multiaddr; +use noosphere_ucan::store::UcanJwtStore; use std::net::Ipv4Addr; -use ucan::store::UcanJwtStore; #[cfg(doc)] use libp2p::kad::Config as KademliaConfig; @@ -17,7 +17,7 @@ use libp2p::kad::Config as KademliaConfig; /// use noosphere_core::authority::generate_ed25519_key; /// use noosphere_storage::{SphereDb, MemoryStorage}; /// use noosphere_ns::{BOOTSTRAP_PEERS, NameSystem, DhtClient, NameSystemBuilder}; -/// use ucan_key_support::ed25519::Ed25519KeyMaterial; +/// use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; /// use tokio; /// /// #[tokio::main(flavor = "multi_thread")] @@ -181,7 +181,7 @@ mod tests { use libp2p::PeerId; use noosphere_core::authority::generate_ed25519_key; use noosphere_storage::{MemoryStorage, SphereDb}; - use ucan_key_support::ed25519::Ed25519KeyMaterial; + use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; #[tokio::test] async fn test_name_system_builder() -> Result<(), anyhow::Error> { diff --git a/rust/noosphere-ns/src/dht/errors.rs b/rust/noosphere-ns/src/dht/errors.rs index 08ef06a68..5bcf8bdeb 100644 --- a/rust/noosphere-ns/src/dht/errors.rs +++ b/rust/noosphere-ns/src/dht/errors.rs @@ -1,4 +1,3 @@ -use anyhow; use libp2p::{kad, kad::store::Error as KadStorageError, TransportError}; use noosphere_common::channel::ChannelError; use std::fmt; diff --git a/rust/noosphere-ns/src/dht/node.rs b/rust/noosphere-ns/src/dht/node.rs index 13885cca4..6bbf7a23c 100644 --- a/rust/noosphere-ns/src/dht/node.rs +++ b/rust/noosphere-ns/src/dht/node.rs @@ -8,7 +8,6 @@ use crate::dht::{ use libp2p::{identity::Keypair, Multiaddr, PeerId}; use noosphere_common::channel::message_channel; use std::time::Duration; -use tokio; macro_rules! ensure_response { ($response:expr, $matcher:pat => $statement:expr) => { diff --git a/rust/noosphere-ns/src/dht/processor.rs b/rust/noosphere-ns/src/dht/processor.rs index 33d0e7633..8fb7644c0 100644 --- a/rust/noosphere-ns/src/dht/processor.rs +++ b/rust/noosphere-ns/src/dht/processor.rs @@ -23,7 +23,6 @@ use libp2p::{ }; use std::{collections::HashMap, time::Duration}; use std::{fmt, num::NonZeroUsize}; -use tokio; /// The processing component of a [DHTNode]/[DHTProcessor] pair. Consumers /// should only interface with a [DHTProcessor] via [DHTNode]. diff --git a/rust/noosphere-ns/src/dht/swarm.rs b/rust/noosphere-ns/src/dht/swarm.rs index 7a2aac6c1..475b0a2eb 100644 --- a/rust/noosphere-ns/src/dht/swarm.rs +++ b/rust/noosphere-ns/src/dht/swarm.rs @@ -12,7 +12,7 @@ use libp2p::{ swarm::{NetworkBehaviour, SwarmEvent}, tls, yamux, PeerId, Swarm, SwarmBuilder, }; -use std::{result::Result, time::Duration}; +use std::time::Duration; use void::Void; /// Protocols are responsible for determining how long diff --git a/rust/noosphere-ns/src/dht_client.rs b/rust/noosphere-ns/src/dht_client.rs index 9dcd0729a..0b63eefc0 100644 --- a/rust/noosphere-ns/src/dht_client.rs +++ b/rust/noosphere-ns/src/dht_client.rs @@ -103,9 +103,9 @@ pub mod test { view::SPHERE_LIFETIME, }; use noosphere_storage::{MemoryStorage, SphereDb}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use std::sync::Arc; use tokio::sync::Mutex; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; pub async fn test_network_info(client: Arc>) -> Result<()> { initialize_tracing(None); diff --git a/rust/noosphere-ns/src/helpers.rs b/rust/noosphere-ns/src/helpers.rs index f9769b42d..9fcab1418 100644 --- a/rust/noosphere-ns/src/helpers.rs +++ b/rust/noosphere-ns/src/helpers.rs @@ -6,9 +6,9 @@ use noosphere_core::{ authority::generate_ed25519_key, data::{Did, LinkRecord}, }; +use noosphere_ucan::store::UcanJwtStore; use std::collections::HashMap; use tokio::sync::Mutex; -use ucan::store::UcanJwtStore; /// An in-process network of [NameSystem] nodes for testing. pub struct NameSystemNetwork { diff --git a/rust/noosphere-ns/src/name_resolver.rs b/rust/noosphere-ns/src/name_resolver.rs index 9489f9c20..baa36a7b7 100644 --- a/rust/noosphere-ns/src/name_resolver.rs +++ b/rust/noosphere-ns/src/name_resolver.rs @@ -48,7 +48,7 @@ pub mod test { tracing::initialize_tracing, view::SPHERE_LIFETIME, }; - use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; + use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; pub async fn test_name_resolver_simple(resolver: N) -> Result<()> { initialize_tracing(None); diff --git a/rust/noosphere-ns/src/name_system.rs b/rust/noosphere-ns/src/name_system.rs index 6e9f8e4f2..618ceb59e 100644 --- a/rust/noosphere-ns/src/name_system.rs +++ b/rust/noosphere-ns/src/name_system.rs @@ -8,8 +8,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use libp2p::{identity::Keypair, Multiaddr}; use noosphere_core::data::{Did, LinkRecord}; -use ucan::{crypto::KeyMaterial, store::UcanJwtStore}; -use ucan_key_support::ed25519::Ed25519KeyMaterial; +use noosphere_ucan::{crypto::KeyMaterial, store::UcanJwtStore}; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; #[cfg(doc)] use cid::Cid; diff --git a/rust/noosphere-ns/src/utils.rs b/rust/noosphere-ns/src/utils.rs index 3335f3d41..5784369a4 100644 --- a/rust/noosphere-ns/src/utils.rs +++ b/rust/noosphere-ns/src/utils.rs @@ -1,5 +1,4 @@ use crate::DhtClient; -use anyhow; use libp2p::{ multiaddr::{Multiaddr, Protocol}, PeerId, diff --git a/rust/noosphere-ns/src/validator.rs b/rust/noosphere-ns/src/validator.rs index 3d8ce6cd2..4660c586d 100644 --- a/rust/noosphere-ns/src/validator.rs +++ b/rust/noosphere-ns/src/validator.rs @@ -1,7 +1,7 @@ use crate::dht::Validator; use async_trait::async_trait; use noosphere_core::data::LinkRecord; -use ucan::store::UcanStore; +use noosphere_ucan::store::UcanStore; /// Implements [Validator] for the DHT. pub(crate) struct RecordValidator { diff --git a/rust/noosphere-ns/tests/ns_test.rs b/rust/noosphere-ns/tests/ns_test.rs index c95fdb9ab..fba1b66d7 100644 --- a/rust/noosphere-ns/tests/ns_test.rs +++ b/rust/noosphere-ns/tests/ns_test.rs @@ -13,8 +13,10 @@ use noosphere_ns::{helpers::NameSystemNetwork, DhtClient}; use noosphere_storage::{derive_cid, MemoryStorage, SphereDb}; use libipld_cbor::DagCborCodec; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore, time::now, Ucan}; -use ucan_key_support::ed25519::Ed25519KeyMaterial; +use noosphere_ucan::{ + builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore, time::now, Ucan, +}; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; /// Data related to an owner and managed sphere identities /// and the publishing tokens it can issue. diff --git a/rust/noosphere-storage/Cargo.toml b/rust/noosphere-storage/Cargo.toml index bd0b40a23..e325fefb7 100644 --- a/rust/noosphere-storage/Cargo.toml +++ b/rust/noosphere-storage/Cargo.toml @@ -10,7 +10,7 @@ categories = [ "web-programming", "web-assembly" ] -rust-version = "1.60.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere-storage" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -19,13 +19,13 @@ readme = "README.md" [dependencies] anyhow = { workspace = true } -async-trait = "~0.1" +async-trait = { workspace = true } async-stream = { workspace = true } tokio-stream = { workspace = true } cid = { workspace = true } -noosphere-common = { version = "0.1.2", path = "../noosphere-common" } +noosphere-common = { workspace = true } +noosphere-ucan = { workspace = true } tracing = "~0.1" -ucan = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } serde = { workspace = true } @@ -37,7 +37,7 @@ witty-phrase-generator = "~0.2" wasm-bindgen-test = { workspace = true } rand = { workspace = true } noosphere-core-dev = { path = "../noosphere-core", features = ["helpers"], package = "noosphere-core" } -noosphere-common = { path = "../noosphere-common", features = ["helpers"] } +noosphere-common = { workspace = true, features = ["helpers"] } instant = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/rust/noosphere-storage/src/db.rs b/rust/noosphere-storage/src/db.rs index da62003f1..6a2f37daa 100644 --- a/rust/noosphere-storage/src/db.rs +++ b/rust/noosphere-storage/src/db.rs @@ -8,11 +8,11 @@ use libipld_core::{ raw::RawCodec, }; use noosphere_common::ConditionalSend; +use noosphere_ucan::store::UcanStore; use serde::{de::DeserializeOwned, Serialize}; use std::future::Future; use std::{collections::BTreeSet, fmt::Debug}; use tokio_stream::Stream; -use ucan::store::UcanStore; use crate::{BlockStore, KeyValueStore, MemoryStore, Storage}; @@ -280,7 +280,7 @@ mod tests { use libipld_cbor::DagCborCodec; use libipld_core::{ipld::Ipld, raw::RawCodec}; - use ucan::store::UcanJwtStore; + use noosphere_ucan::store::UcanJwtStore; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; diff --git a/rust/noosphere-storage/src/ucan.rs b/rust/noosphere-storage/src/ucan.rs index 0b238f57f..55d038b17 100644 --- a/rust/noosphere-storage/src/ucan.rs +++ b/rust/noosphere-storage/src/ucan.rs @@ -6,7 +6,7 @@ use libipld_core::{ codec::{Decode, Encode}, raw::RawCodec, }; -use ucan::store::{UcanStore as UcanStoreTrait, UcanStoreConditionalSend}; +use noosphere_ucan::store::{UcanStore as UcanStoreTrait, UcanStoreConditionalSend}; use crate::block::BlockStore; diff --git a/rust/noosphere-ucan-key-support/Cargo.toml b/rust/noosphere-ucan-key-support/Cargo.toml new file mode 100644 index 000000000..c302aacef --- /dev/null +++ b/rust/noosphere-ucan-key-support/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "noosphere-ucan-key-support" +version = "0.1.7" +edition = "2021" +description = "Ready to use SigningKey implementations for the ucan crate" +keywords = ["ucan", "authz", "jwt", "pki"] +categories = [ + "authorization", + "cryptography", + "encoding", + "web-programming" +] +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/noosphere-ucan-key-support" +repository = "https://github.com/subconsciousnetwork/noosphere" +homepage = "https://github.com/subconsciousnetwork/noosphere" +readme = "README.md" +rust-version = "1.75.0" + +[features] +default = [] + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bs58 = { workspace = true } +ed25519-zebra = { workspace = true } +log = "0.4" +rsa = "0.9" +p256 = "0.13" +sha2 = { version = "0.10", features = ["oid"] } +noosphere-ucan = { workspace = true } + +[build-dependencies] +npm_rs = "1.0" + +[dev-dependencies] +# NOTE: This is needed so that rand can be included in WASM builds +getrandom = { workspace = true, features = ["js"] } +rand = { workspace = true } +wasm-bindgen-test = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +workspace = true +features = [ + 'Window', + 'SubtleCrypto', + 'Crypto', + 'CryptoKey', + 'CryptoKeyPair', + 'DedicatedWorkerGlobalScope' +] + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +pollster = "0.3.0" diff --git a/rust/noosphere-ucan-key-support/README.md b/rust/noosphere-ucan-key-support/README.md new file mode 100644 index 000000000..0e0728d17 --- /dev/null +++ b/rust/noosphere-ucan-key-support/README.md @@ -0,0 +1,10 @@ +![API Stability: Alpha](https://img.shields.io/badge/API%20Stability-Alpha-red) + +# Noosphere Ucan Key Support + +This is an auxilliary crate containing ready-to-use `SigningKey` implementations +for the [Rust UCAN implementation][noosphere-ucan]. + +Forked from [rs-ucan](https://github.com/ucan-wg/rs-ucan). + +[noosphere-ucan]: https://docs.rs/noosphere-ucan diff --git a/rust/noosphere-ucan-key-support/src/ed25519.rs b/rust/noosphere-ucan-key-support/src/ed25519.rs new file mode 100644 index 000000000..ad062e580 --- /dev/null +++ b/rust/noosphere-ucan-key-support/src/ed25519.rs @@ -0,0 +1,92 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; + +use ed25519_zebra::{ + Signature, SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey, +}; + +use noosphere_ucan::crypto::KeyMaterial; + +pub use noosphere_ucan::crypto::{did::ED25519_MAGIC_BYTES, JwtSignatureAlgorithm}; + +pub fn bytes_to_ed25519_key(bytes: Vec) -> Result> { + let public_key = Ed25519PublicKey::try_from(bytes.as_slice())?; + Ok(Box::new(Ed25519KeyMaterial(public_key, None))) +} + +#[derive(Clone)] +pub struct Ed25519KeyMaterial(pub Ed25519PublicKey, pub Option); + +#[cfg_attr(target_arch="wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl KeyMaterial for Ed25519KeyMaterial { + fn get_jwt_algorithm_name(&self) -> String { + JwtSignatureAlgorithm::EdDSA.to_string() + } + + async fn get_did(&self) -> Result { + let bytes = [ED25519_MAGIC_BYTES, self.0.as_ref()].concat(); + Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) + } + + async fn sign(&self, payload: &[u8]) -> Result> { + match self.1 { + Some(private_key) => { + let signature = private_key.sign(payload); + let bytes: [u8; 64] = signature.into(); + Ok(bytes.to_vec()) + } + None => Err(anyhow!("No private key; cannot sign data")), + } + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let signature = Signature::try_from(signature)?; + self.0 + .verify(&signature, payload) + .map_err(|error| anyhow!("Could not verify signature: {:?}", error)) + } +} + +#[cfg(test)] +mod tests { + use super::{bytes_to_ed25519_key, Ed25519KeyMaterial, ED25519_MAGIC_BYTES}; + use ed25519_zebra::{SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey}; + use noosphere_ucan::{ + builder::UcanBuilder, + crypto::{did::DidParser, KeyMaterial}, + ucan::Ucan, + }; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_sign_and_verify_a_ucan() { + let rng = rand::thread_rng(); + let private_key = Ed25519PrivateKey::new(rng); + let public_key = Ed25519PublicKey::from(&private_key); + + let key_material = Ed25519KeyMaterial(public_key, Some(private_key)); + let token_string = UcanBuilder::default() + .issued_by(&key_material) + .for_audience(key_material.get_did().await.unwrap().as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut did_parser = DidParser::new(&[(ED25519_MAGIC_BYTES, bytes_to_ed25519_key)]); + + let ucan = Ucan::try_from(token_string).unwrap(); + ucan.check_signature(&mut did_parser).await.unwrap(); + } +} diff --git a/rust/noosphere-ucan-key-support/src/fixtures/rsa_key.pk8 b/rust/noosphere-ucan-key-support/src/fixtures/rsa_key.pk8 new file mode 100644 index 000000000..1af064cc8 Binary files /dev/null and b/rust/noosphere-ucan-key-support/src/fixtures/rsa_key.pk8 differ diff --git a/rust/noosphere-ucan-key-support/src/lib.rs b/rust/noosphere-ucan-key-support/src/lib.rs new file mode 100644 index 000000000..e890a1b56 --- /dev/null +++ b/rust/noosphere-ucan-key-support/src/lib.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate log; + +#[cfg(target_arch = "wasm32")] +pub mod web_crypto; + +pub mod ed25519; +pub mod p256; +pub mod rsa; diff --git a/rust/noosphere-ucan-key-support/src/p256.rs b/rust/noosphere-ucan-key-support/src/p256.rs new file mode 100644 index 000000000..c87f165c7 --- /dev/null +++ b/rust/noosphere-ucan-key-support/src/p256.rs @@ -0,0 +1,93 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; + +use p256::ecdsa::{ + self, + signature::{Signer, Verifier}, + Signature, SigningKey as P256PrivateKey, VerifyingKey as P256PublicKey, +}; + +use noosphere_ucan::crypto::KeyMaterial; + +pub use noosphere_ucan::crypto::{did::P256_MAGIC_BYTES, JwtSignatureAlgorithm}; + +pub fn bytes_to_p256_key(bytes: Vec) -> Result> { + let public_key = P256PublicKey::try_from(bytes.as_slice())?; + Ok(Box::new(P256KeyMaterial(public_key, None))) +} + +/// Support for NIST P-256 keys, aka secp256r1, aka ES256 +#[derive(Clone)] +pub struct P256KeyMaterial(pub P256PublicKey, pub Option); + +#[cfg_attr(target_arch="wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl KeyMaterial for P256KeyMaterial { + fn get_jwt_algorithm_name(&self) -> String { + JwtSignatureAlgorithm::ES256.to_string() + } + + async fn get_did(&self) -> Result { + let bytes = [P256_MAGIC_BYTES, &self.0.to_encoded_point(true).to_bytes()].concat(); + Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) + } + + async fn sign(&self, payload: &[u8]) -> Result> { + match self.1 { + Some(ref private_key) => { + let signature: ecdsa::Signature = private_key.sign(payload); + Ok(signature.to_vec()) + } + None => Err(anyhow!("No private key; cannot sign data")), + } + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let signature = Signature::try_from(signature)?; + self.0 + .verify(payload, &signature) + .map_err(|error| anyhow!("Could not verify signature: {:?}", error)) + } +} + +#[cfg(test)] +mod tests { + use super::{bytes_to_p256_key, P256KeyMaterial, P256_MAGIC_BYTES}; + use noosphere_ucan::{ + builder::UcanBuilder, + crypto::{did::DidParser, KeyMaterial}, + ucan::Ucan, + }; + use p256::ecdsa::{SigningKey as P256PrivateKey, VerifyingKey as P256PublicKey}; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_sign_and_verify_a_ucan() { + let private_key = P256PrivateKey::random(&mut p256::elliptic_curve::rand_core::OsRng); + let public_key = P256PublicKey::from(&private_key); + + let key_material = P256KeyMaterial(public_key, Some(private_key)); + let token_string = UcanBuilder::default() + .issued_by(&key_material) + .for_audience(key_material.get_did().await.unwrap().as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut did_parser = DidParser::new(&[(P256_MAGIC_BYTES, bytes_to_p256_key)]); + + let ucan = Ucan::try_from(token_string).unwrap(); + ucan.check_signature(&mut did_parser).await.unwrap(); + } +} diff --git a/rust/noosphere-ucan-key-support/src/rsa.rs b/rust/noosphere-ucan-key-support/src/rsa.rs new file mode 100644 index 000000000..ec9935568 --- /dev/null +++ b/rust/noosphere-ucan-key-support/src/rsa.rs @@ -0,0 +1,114 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; + +use rsa::{ + pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}, + Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, +}; + +use noosphere_ucan::crypto::{JwtSignatureAlgorithm, KeyMaterial}; +use sha2::{Digest, Sha256}; + +pub use noosphere_ucan::crypto::did::RSA_MAGIC_BYTES; + +pub fn bytes_to_rsa_key(bytes: Vec) -> Result> { + println!("Trying to parse RSA key..."); + // NOTE: DID bytes are PKCS1, but we store RSA keys as PKCS8 + let public_key = RsaPublicKey::from_pkcs1_der(&bytes)?; + + Ok(Box::new(RsaKeyMaterial(public_key, None))) +} + +#[derive(Clone)] +pub struct RsaKeyMaterial(pub RsaPublicKey, pub Option); + +#[cfg_attr(target_arch="wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl KeyMaterial for RsaKeyMaterial { + fn get_jwt_algorithm_name(&self) -> String { + JwtSignatureAlgorithm::RS256.to_string() + } + + async fn get_did(&self) -> Result { + let bytes = match self.0.to_pkcs1_der() { + Ok(document) => [RSA_MAGIC_BYTES, document.as_bytes()].concat(), + Err(error) => { + // TODO: Probably shouldn't swallow this error... + warn!("Could not get RSA public key bytes for DID: {:?}", error); + Vec::new() + } + }; + Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) + } + + async fn sign(&self, payload: &[u8]) -> Result> { + let mut hasher = Sha256::new(); + hasher.update(payload); + let hashed = hasher.finalize(); + + match &self.1 { + Some(private_key) => { + let padding = Pkcs1v15Sign::new::(); + let signature = private_key.sign(padding, hashed.as_ref())?; + info!("SIGNED!"); + Ok(signature) + } + None => Err(anyhow!("No private key; cannot sign data")), + } + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let mut hasher = Sha256::new(); + hasher.update(payload); + let hashed = hasher.finalize(); + let padding = Pkcs1v15Sign::new::(); + + self.0 + .verify(padding, hashed.as_ref(), signature) + .map_err(|error| anyhow!(error)) + } +} + +#[cfg(test)] +mod tests { + use super::{bytes_to_rsa_key, RsaKeyMaterial, RSA_MAGIC_BYTES}; + + use noosphere_ucan::{ + builder::UcanBuilder, + crypto::{did::DidParser, KeyMaterial}, + ucan::Ucan, + }; + use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey, RsaPublicKey}; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_sign_and_verify_a_ucan() { + let private_key = + RsaPrivateKey::from_pkcs8_der(include_bytes!("./fixtures/rsa_key.pk8")).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + + let key_material = RsaKeyMaterial(public_key, Some(private_key)); + let token_string = UcanBuilder::default() + .issued_by(&key_material) + .for_audience(key_material.get_did().await.unwrap().as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); + + let ucan = Ucan::try_from(token_string).unwrap(); + ucan.check_signature(&mut did_parser).await.unwrap(); + } +} diff --git a/rust/noosphere-ucan-key-support/src/web_crypto.rs b/rust/noosphere-ucan-key-support/src/web_crypto.rs new file mode 100644 index 000000000..37626da81 --- /dev/null +++ b/rust/noosphere-ucan-key-support/src/web_crypto.rs @@ -0,0 +1,261 @@ +use crate::rsa::RsaKeyMaterial; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use js_sys::{Array, ArrayBuffer, Boolean, Object, Reflect, Uint8Array}; +use noosphere_ucan::crypto::{JwtSignatureAlgorithm, KeyMaterial}; +use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey}; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{Crypto, CryptoKey, CryptoKeyPair, SubtleCrypto}; + +pub fn convert_spki_to_rsa_public_key(spki_bytes: &[u8]) -> Result> { + // TODO: This is maybe a not-good, hacky solution; verifying the first + // 24 bytes would be more wholesome + // SEE: https://github.com/ucan-wg/ts-ucan/issues/30#issuecomment-1007333500 + Ok(Vec::from(&spki_bytes[24..])) +} + +pub const WEB_CRYPTO_RSA_ALGORITHM: &str = "RSASSA-PKCS1-v1_5"; + +#[derive(Debug)] +pub struct WebCryptoRsaKeyMaterial(pub CryptoKey, pub Option); + +impl WebCryptoRsaKeyMaterial { + fn get_subtle_crypto() -> Result { + // NOTE: Accessing either `Window` or `DedicatedWorkerGlobalScope` in + // a context where they are not defined will cause a JS error, so we + // do a sneaky workaround here: + let global = js_sys::global(); + match Reflect::get(&global, &JsValue::from("crypto")) { + Ok(value) => Ok(value.dyn_into::().expect("Unexpected API").subtle()), + _ => Err(anyhow!("Could not access WebCrypto API")), + } + } + + fn private_key(&self) -> Result<&CryptoKey> { + match &self.1 { + Some(key) => Ok(key), + None => Err(anyhow!("No private key configured")), + } + } + + pub async fn generate(key_size: Option) -> Result { + let subtle_crypto = Self::get_subtle_crypto()?; + let algorithm = Object::new(); + + Reflect::set( + &algorithm, + &JsValue::from("name"), + &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + Reflect::set( + &algorithm, + &JsValue::from("modulusLength"), + &JsValue::from(key_size.unwrap_or(2048)), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + let public_exponent = Uint8Array::new(&JsValue::from(3u8)); + public_exponent.copy_from(&[0x01u8, 0x00, 0x01]); + + Reflect::set( + &algorithm, + &JsValue::from("publicExponent"), + &JsValue::from(public_exponent), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + let hash = Object::new(); + + Reflect::set(&hash, &JsValue::from("name"), &JsValue::from("SHA-256")) + .map_err(|error| anyhow!("{:?}", error))?; + + Reflect::set(&algorithm, &JsValue::from("hash"), &JsValue::from(hash)) + .map_err(|error| anyhow!("{:?}", error))?; + + let uses = Array::new(); + + uses.push(&JsValue::from("sign")); + uses.push(&JsValue::from("verify")); + + let crypto_key_pair_generates = subtle_crypto + .generate_key_with_object(&algorithm, false, &uses) + .map_err(|error| anyhow!("{:?}", error))?; + let crypto_key_pair = CryptoKeyPair::from( + JsFuture::from(crypto_key_pair_generates) + .await + .map_err(|error| anyhow!("{:?}", error))?, + ); + + let public_key = CryptoKey::from( + Reflect::get(&crypto_key_pair, &JsValue::from("publicKey")) + .map_err(|error| anyhow!("{:?}", error))?, + ); + let private_key = CryptoKey::from( + Reflect::get(&crypto_key_pair, &JsValue::from("privateKey")) + .map_err(|error| anyhow!("{:?}", error))?, + ); + + Ok(WebCryptoRsaKeyMaterial(public_key, Some(private_key))) + } +} + +#[async_trait(?Send)] +impl KeyMaterial for WebCryptoRsaKeyMaterial { + fn get_jwt_algorithm_name(&self) -> String { + JwtSignatureAlgorithm::RS256.to_string() + } + + async fn get_did(&self) -> Result { + let public_key = &self.0; + let subtle_crypto = Self::get_subtle_crypto()?; + + let public_key_bytes = Uint8Array::new( + &JsFuture::from( + subtle_crypto + .export_key("spki", public_key) + .expect("Could not access key extraction API"), + ) + .await + .expect("Failed to extract public key bytes") + .dyn_into::() + .expect("Bytes were not an ArrayBuffer"), + ); + + let public_key_bytes = public_key_bytes.to_vec(); + let public_key_bytes = convert_spki_to_rsa_public_key(public_key_bytes.as_slice())?; + let public_key = RsaPublicKey::from_pkcs1_der(&public_key_bytes)?; + + Ok(RsaKeyMaterial(public_key, None).get_did().await?) + } + + async fn sign(&self, payload: &[u8]) -> Result> { + let key = self.private_key()?; + let subtle_crypto = Self::get_subtle_crypto()?; + let algorithm = Object::new(); + + Reflect::set( + &algorithm, + &JsValue::from("name"), + &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + Reflect::set( + &algorithm, + &JsValue::from("saltLength"), + &JsValue::from(128u8), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + let data = unsafe { Uint8Array::view(payload) }; + + let result = Uint8Array::new( + &JsFuture::from( + subtle_crypto + .sign_with_object_and_buffer_source(&algorithm, key, &data) + .map_err(|error| anyhow!("{:?}", error))?, + ) + .await + .map_err(|error| anyhow!("{:?}", error))?, + ); + + Ok(result.to_vec()) + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let key = &self.0; + let subtle_crypto = Self::get_subtle_crypto()?; + let algorithm = Object::new(); + + Reflect::set( + &algorithm, + &JsValue::from("name"), + &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), + ) + .map_err(|error| anyhow!("{:?}", error))?; + Reflect::set( + &algorithm, + &JsValue::from("saltLength"), + &JsValue::from(128u8), + ) + .map_err(|error| anyhow!("{:?}", error))?; + + let signature = unsafe { Uint8Array::view(signature) }; + let data = unsafe { Uint8Array::view(payload) }; + + let valid = JsFuture::from( + subtle_crypto + .verify_with_object_and_buffer_source_and_buffer_source( + &algorithm, key, &signature, &data, + ) + .map_err(|error| anyhow!("{:?}", error))?, + ) + .await + .map_err(|error| anyhow!("{:?}", error))? + .dyn_into::() + .map_err(|error| anyhow!("{:?}", error))?; + + match valid.is_truthy() { + true => Ok(()), + false => Err(anyhow!("Could not verify signature")), + } + } +} + +#[cfg(test)] +mod tests { + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + use super::WebCryptoRsaKeyMaterial; + use crate::rsa::{bytes_to_rsa_key, RSA_MAGIC_BYTES}; + use noosphere_ucan::{ + builder::UcanBuilder, + crypto::{did::DidParser, KeyMaterial}, + ucan::Ucan, + }; + + #[wasm_bindgen_test] + async fn it_can_sign_and_verify_data() { + let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); + let data = &[0xdeu8, 0xad, 0xbe, 0xef]; + let signature = key_material.sign(data).await.unwrap(); + + key_material.verify(data, signature.as_ref()).await.unwrap(); + } + + #[wasm_bindgen_test] + async fn it_produces_a_legible_rsa_did() { + let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); + let did = key_material.get_did().await.unwrap(); + let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); + + did_parser.parse(&did).unwrap(); + } + + #[wasm_bindgen_test] + async fn it_signs_ucans_that_can_be_verified_elsewhere() { + let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); + + let token = UcanBuilder::default() + .issued_by(&key_material) + .for_audience(key_material.get_did().await.unwrap().as_str()) + .with_lifetime(300) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); + let ucan = Ucan::try_from(token.as_str()).unwrap(); + + ucan.check_signature(&mut did_parser).await.unwrap(); + } +} diff --git a/rust/noosphere-ucan/Cargo.toml b/rust/noosphere-ucan/Cargo.toml new file mode 100644 index 000000000..950227ce1 --- /dev/null +++ b/rust/noosphere-ucan/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "noosphere-ucan" +version = "0.4.0" +edition = "2021" +description = "Implement UCAN-based authorization with conciseness and ease!" +keywords = ["ucan", "authz", "jwt", "pki"] +categories = [ + "authentication", + "cryptography", + "encoding", + "web-programming" +] +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/noosphere-ucan" +repository = "https://github.com/subconsciousnetwork/noosphere" +homepage = "https://github.com/subconsciousnetwork/noosphere" +readme = "README.md" +rust-version = "1.75.0" + +[features] +default = [] + +[dependencies] +anyhow = { workspace = true } +async-recursion = { workspace = true } +async-trait = { workspace = true } +base64 = { workspace = true } +bs58 = { workspace = true } +cid = { workspace = true } +futures = { workspace = true } +instant = { workspace = true, features = ["wasm-bindgen"] } +libipld-core = { workspace = true, features = ["serde-codec", "serde"] } +libipld-json = { workspace = true } +log = "0.4" +rand = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +unsigned-varint = { workspace = true } +url = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +# NOTE: This is needed so that rand can be included in WASM builds +getrandom = { workspace = true, features = ["js"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "test-util"] } + +[dev-dependencies] +did-key = "0.2" +serde_ipld_dagcbor = { workspace = true } +wasm-bindgen-test = { workspace = true } diff --git a/rust/noosphere-ucan/README.md b/rust/noosphere-ucan/README.md new file mode 100644 index 000000000..40386f36a --- /dev/null +++ b/rust/noosphere-ucan/README.md @@ -0,0 +1,14 @@ +![API Stability: Alpha](https://img.shields.io/badge/API%20Stability-Alpha-red) + +# Noosphere Ucan + +This is a Rust library to help the next generation of web applications make use +of UCANs in their authorization flows. To learn more about UCANs and how you +might use them in your application, visit [https://ucan.xyz][ucan website] or +read the [spec][spec]. + +This project was forked from the official [rs-ucan] implementation, pending the finalization and broad adoption of the v1.0.0 spec (which represents a major breaking change from v0.10.0). + +[spec]: https://github.com/ucan-wg/spec +[ucan website]: https://ucan.xyz +[rs-ucan]: https://github.com/ucan-wg/rs-ucan diff --git a/rust/noosphere-ucan/src/builder.rs b/rust/noosphere-ucan/src/builder.rs new file mode 100644 index 000000000..44fb9cb99 --- /dev/null +++ b/rust/noosphere-ucan/src/builder.rs @@ -0,0 +1,310 @@ +use std::collections::BTreeMap; + +use crate::{ + capability::{proof::ProofDelegationSemantics, Capability, CapabilitySemantics}, + crypto::KeyMaterial, + serde::Base64Encode, + time::now, + ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, +}; +use anyhow::{anyhow, Result}; +use base64::Engine; +use cid::multihash::Code; +use log::warn; +use rand::Rng; +use serde::{de::DeserializeOwned, Serialize}; + +/// A signable is a UCAN that has all the state it needs in order to be signed, +/// but has not yet been signed. +/// NOTE: This may be useful for bespoke signing flows down the road. It is +/// meant to approximate the way that ts-ucan produces an unsigned intermediate +/// artifact (e.g., ) +pub struct Signable<'a, K> +where + K: KeyMaterial, +{ + pub issuer: &'a K, + pub audience: String, + + pub capabilities: Vec, + + pub expiration: Option, + pub not_before: Option, + + pub facts: FactsMap, + pub proofs: Vec, + pub add_nonce: bool, +} + +impl<'a, K> Signable<'a, K> +where + K: KeyMaterial, +{ + /// The header field components of the UCAN JWT + pub fn ucan_header(&self) -> UcanHeader { + UcanHeader { + alg: self.issuer.get_jwt_algorithm_name(), + typ: "JWT".into(), + } + } + + /// The payload field components of the UCAN JWT + pub async fn ucan_payload(&self) -> Result { + let nonce = match self.add_nonce { + true => Some( + base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(rand::thread_rng().gen::<[u8; 32]>()), + ), + false => None, + }; + + let facts = if self.facts.is_empty() { + None + } else { + Some(self.facts.clone()) + }; + + let proofs = if self.proofs.is_empty() { + None + } else { + Some(self.proofs.clone()) + }; + + Ok(UcanPayload { + ucv: UCAN_VERSION.into(), + aud: self.audience.clone(), + iss: self.issuer.get_did().await?, + exp: self.expiration, + nbf: self.not_before, + nnc: nonce, + cap: self.capabilities.clone().try_into()?, + fct: facts, + prf: proofs, + }) + } + + /// Produces a Ucan, which contains finalized UCAN fields along with signed + /// data suitable for encoding as a JWT token string + pub async fn sign(&self) -> Result { + let header = self.ucan_header(); + let payload = self + .ucan_payload() + .await + .expect("Unable to generate UCAN payload"); + + let header_base64 = header.jwt_base64_encode()?; + let payload_base64 = payload.jwt_base64_encode()?; + + let data_to_sign = format!("{header_base64}.{payload_base64}") + .as_bytes() + .to_vec(); + let signature = self.issuer.sign(data_to_sign.as_slice()).await?; + + Ok(Ucan::new(header, payload, data_to_sign, signature)) + } +} + +/// A builder API for UCAN tokens +#[derive(Clone)] +pub struct UcanBuilder<'a, K> +where + K: KeyMaterial, +{ + issuer: Option<&'a K>, + audience: Option, + + capabilities: Vec, + + lifetime: Option, + expiration: Option, + not_before: Option, + + facts: FactsMap, + proofs: Vec, + add_nonce: bool, +} + +impl<'a, K> Default for UcanBuilder<'a, K> +where + K: KeyMaterial, +{ + /// Create an empty builder. + /// Before finalising the builder, you need to at least call: + /// + /// - `issued_by` + /// - `to_audience` and one of + /// - `with_lifetime` or `with_expiration`. + /// + /// To finalise the builder, call its `build` or `build_parts` method. + fn default() -> Self { + UcanBuilder { + issuer: None, + audience: None, + + capabilities: Vec::new(), + + lifetime: None, + expiration: None, + not_before: None, + + facts: BTreeMap::new(), + proofs: Vec::new(), + add_nonce: false, + } + } +} + +impl<'a, K> UcanBuilder<'a, K> +where + K: KeyMaterial, +{ + /// The UCAN must be signed with the private key of the issuer to be valid. + pub fn issued_by(mut self, issuer: &'a K) -> Self { + self.issuer = Some(issuer); + self + } + + /// This is the identity this UCAN transfers rights to. + /// + /// It could e.g. be the DID of a service you're posting this UCAN as a JWT to, + /// or it could be the DID of something that'll use this UCAN as a proof to + /// continue the UCAN chain as an issuer. + pub fn for_audience(mut self, audience: &str) -> Self { + self.audience = Some(String::from(audience)); + self + } + + /// The number of seconds into the future (relative to when build() is + /// invoked) to set the expiration. This is ignored if an explicit expiration + /// is set. + pub fn with_lifetime(mut self, seconds: u64) -> Self { + self.lifetime = Some(seconds); + self + } + + /// Set the POSIX timestamp (in seconds) for when the UCAN should expire. + /// Setting this value overrides a configured lifetime value. + pub fn with_expiration(mut self, timestamp: u64) -> Self { + self.expiration = Some(timestamp); + self + } + + /// Set the POSIX timestamp (in seconds) of when the UCAN becomes active. + pub fn not_before(mut self, timestamp: u64) -> Self { + self.not_before = Some(timestamp); + self + } + + /// Add a fact or proof of knowledge to this UCAN. + pub fn with_fact(mut self, key: &str, fact: T) -> Self { + match serde_json::to_value(fact) { + Ok(value) => { + self.facts.insert(key.to_owned(), value); + } + Err(error) => warn!("Could not add fact to UCAN: {}", error), + } + self + } + + /// Will ensure that the built UCAN includes a number used once. + pub fn with_nonce(mut self) -> Self { + self.add_nonce = true; + self + } + + /// Includes a UCAN in the list of proofs for the UCAN to be built. + /// Note that the proof's audience must match this UCAN's issuer + /// or else the proof chain will be invalidated! + /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] + /// algorithm, unless one is provided. + pub fn witnessed_by(mut self, authority: &Ucan, hasher: Option) -> Self { + match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { + Ok(proof) => self.proofs.push(proof.to_string()), + Err(error) => warn!("Failed to add authority to proofs: {}", error), + } + + self + } + + /// Claim a capability by inheritance (from an authorizing proof) or + /// implicitly by ownership of the resource by this UCAN's issuer + pub fn claiming_capability(mut self, capability: C) -> Self + where + C: Into, + { + self.capabilities.push(capability.into()); + self + } + + /// Claim capabilities by inheritance (from an authorizing proof) or + /// implicitly by ownership of the resource by this UCAN's issuer + pub fn claiming_capabilities(mut self, capabilities: &[C]) -> Self + where + C: Into + Clone, + { + let caps: Vec = capabilities + .iter() + .map(|c| >::into(c.to_owned())) + .collect(); + self.capabilities.extend(caps); + self + } + + /// Delegate all capabilities from a given proof to the audience of the UCAN + /// you're building. + /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] + /// algorithm, unless one is provided. + pub fn delegating_from(mut self, authority: &Ucan, hasher: Option) -> Self { + match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { + Ok(proof) => { + self.proofs.push(proof.to_string()); + let proof_index = self.proofs.len() - 1; + let proof_delegation = ProofDelegationSemantics {}; + let capability = + proof_delegation.parse(&format!("prf:{proof_index}"), "ucan/DELEGATE", None); + + match capability { + Some(capability) => { + self.capabilities.push(Capability::from(&capability)); + } + None => warn!("Could not produce delegation capability"), + } + } + Err(error) => warn!("Could not encode authoritative UCAN: {:?}", error), + }; + + self + } + + /// Returns the default hasher ([Code::Blake3_256]) used for [Cid] encodings. + pub fn default_hasher() -> Code { + Code::Blake3_256 + } + + fn implied_expiration(&self) -> Option { + if self.expiration.is_some() { + self.expiration + } else { + self.lifetime.map(|lifetime| now() + lifetime) + } + } + + pub fn build(self) -> Result> { + match &self.issuer { + Some(issuer) => match &self.audience { + Some(audience) => Ok(Signable { + issuer, + audience: audience.clone(), + not_before: self.not_before, + expiration: self.implied_expiration(), + facts: self.facts.clone(), + capabilities: self.capabilities.clone(), + proofs: self.proofs.clone(), + add_nonce: self.add_nonce, + }), + None => Err(anyhow!("Missing audience")), + }, + None => Err(anyhow!("Missing issuer")), + } + } +} diff --git a/rust/noosphere-ucan/src/capability/caveats.rs b/rust/noosphere-ucan/src/capability/caveats.rs new file mode 100644 index 000000000..b7e53a0ee --- /dev/null +++ b/rust/noosphere-ucan/src/capability/caveats.rs @@ -0,0 +1,86 @@ +use std::ops::Deref; + +use anyhow::{anyhow, Error, Result}; +use serde_json::{Map, Value}; + +#[derive(Clone)] +pub struct Caveat(Map); + +impl Caveat { + /// Determines if this [Caveat] enables/allows the provided caveat. + /// + /// ``` + /// use noosphere_ucan::capability::{Caveat}; + /// use serde_json::json; + /// + /// let no_caveat = Caveat::try_from(json!({})).unwrap(); + /// let x_caveat = Caveat::try_from(json!({ "x": true })).unwrap(); + /// let x_diff_caveat = Caveat::try_from(json!({ "x": false })).unwrap(); + /// let y_caveat = Caveat::try_from(json!({ "y": true })).unwrap(); + /// let xz_caveat = Caveat::try_from(json!({ "x": true, "z": true })).unwrap(); + /// + /// assert!(no_caveat.enables(&no_caveat)); + /// assert!(x_caveat.enables(&x_caveat)); + /// assert!(no_caveat.enables(&x_caveat)); + /// assert!(x_caveat.enables(&xz_caveat)); + /// + /// assert!(!x_caveat.enables(&x_diff_caveat)); + /// assert!(!x_caveat.enables(&no_caveat)); + /// assert!(!x_caveat.enables(&y_caveat)); + /// ``` + pub fn enables(&self, other: &Caveat) -> bool { + if self.is_empty() { + return true; + } + + if other.is_empty() { + return false; + } + + if self == other { + return true; + } + + for (key, value) in self.iter() { + if let Some(other_value) = other.get(key) { + if value != other_value { + return false; + } + } else { + return false; + } + } + + true + } +} + +impl Deref for Caveat { + type Target = Map; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for Caveat { + fn eq(&self, other: &Caveat) -> bool { + self.0 == other.0 + } +} + +impl TryFrom for Caveat { + type Error = Error; + fn try_from(value: Value) -> Result { + Ok(Caveat(match value { + Value::Object(obj) => obj, + _ => return Err(anyhow!("Caveat must be an object")), + })) + } +} + +impl TryFrom<&Value> for Caveat { + type Error = Error; + fn try_from(value: &Value) -> Result { + Caveat::try_from(value.to_owned()) + } +} diff --git a/rust/noosphere-ucan/src/capability/data.rs b/rust/noosphere-ucan/src/capability/data.rs new file mode 100644 index 000000000..37cd5fe67 --- /dev/null +++ b/rust/noosphere-ucan/src/capability/data.rs @@ -0,0 +1,229 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + collections::{btree_map::Iter as BTreeMapIter, BTreeMap}, + fmt::Debug, + iter::FlatMap, + ops::Deref, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Represents a single, flattened capability containing a resource, ability, and caveat. +pub struct Capability { + pub resource: String, + pub ability: String, + pub caveat: Value, +} + +impl Capability { + pub fn new(resource: String, ability: String, caveat: Value) -> Self { + Capability { + resource, + ability, + caveat, + } + } +} + +impl From<&Capability> for Capability { + fn from(value: &Capability) -> Self { + value.to_owned() + } +} + +impl From<(String, String, Value)> for Capability { + fn from(value: (String, String, Value)) -> Self { + Capability::new(value.0, value.1, value.2) + } +} + +impl From<(&str, &str, &Value)> for Capability { + fn from(value: (&str, &str, &Value)) -> Self { + Capability::new(value.0.to_owned(), value.1.to_owned(), value.2.to_owned()) + } +} + +impl From for (String, String, Value) { + fn from(value: Capability) -> Self { + (value.resource, value.ability, value.caveat) + } +} + +type MapImpl = BTreeMap; +type MapIter<'a, K, V> = BTreeMapIter<'a, K, V>; +type AbilitiesImpl = MapImpl>; +type CapabilitiesImpl = MapImpl; +type AbilitiesMapClosure<'a> = Box)) -> Vec + 'a>; +type AbilitiesMap<'a> = + FlatMap>, Vec, AbilitiesMapClosure<'a>>; +type CapabilitiesIterator<'a> = FlatMap< + MapIter<'a, String, AbilitiesImpl>, + AbilitiesMap<'a>, + fn((&'a String, &'a AbilitiesImpl)) -> AbilitiesMap<'a>, +>; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +/// The [Capabilities] struct contains capability data as a map-of-maps, matching the +/// [spec](https://github.com/ucan-wg/spec#326-capabilities--attenuation). +/// See `iter()` to deconstruct this map into a sequence of [Capability] datas. +/// +/// ``` +/// use noosphere_ucan::capability::Capabilities; +/// use serde_json::json; +/// +/// let capabilities = Capabilities::try_from(&json!({ +/// "mailto:username@example.com": { +/// "msg/receive": [{}], +/// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] +/// } +/// })).unwrap(); +/// +/// let resource = capabilities.get("mailto:username@example.com").unwrap(); +/// assert_eq!(resource.get("msg/receive").unwrap(), &vec![json!({})]); +/// assert_eq!(resource.get("msg/send").unwrap(), &vec![json!({ "draft": true }), json!({ "publish": true, "topic": ["foo"] })]) +/// ``` +pub struct Capabilities(CapabilitiesImpl); + +impl Capabilities { + /// Using a [FlatMap] implementation, iterate over a [Capabilities] map-of-map + /// as a sequence of [Capability] datas. + /// + /// ``` + /// use noosphere_ucan::capability::{Capabilities, Capability}; + /// use serde_json::json; + /// + /// let capabilities = Capabilities::try_from(&json!({ + /// "example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr": { + /// "wnfs/append": [{}] + /// }, + /// "mailto:username@example.com": { + /// "msg/receive": [{}], + /// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] + /// } + /// })).unwrap(); + /// + /// assert_eq!(capabilities.iter().collect::>(), vec![ + /// Capability::from(("example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr", "wnfs/append", &json!({}))), + /// Capability::from(("mailto:username@example.com", "msg/receive", &json!({}))), + /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "draft": true }))), + /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "publish": true, "topic": ["foo"] }))), + /// ]); + /// ``` + pub fn iter(&self) -> CapabilitiesIterator { + self.0 + .iter() + .flat_map(|(resource, abilities): (&String, &AbilitiesImpl)| { + abilities + .iter() + .flat_map(Box::new( + |(ability, caveats): (&String, &Vec)| match caveats.len() { + 0 => vec![], // An empty caveats list is the same as no capability at all + _ => caveats + .iter() + .map(|caveat| { + Capability::from(( + resource.to_owned(), + ability.to_owned(), + caveat.to_owned(), + )) + }) + .collect(), + }, + )) + }) + } +} + +impl Deref for Capabilities { + type Target = CapabilitiesImpl; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for Capabilities { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + let mut resources: CapabilitiesImpl = BTreeMap::new(); + for capability in value.into_iter() { + let (resource_name, ability, caveat) = <(String, String, Value)>::from(capability); + + let resource = if let Some(resource) = resources.get_mut(&resource_name) { + resource + } else { + let resource: AbilitiesImpl = BTreeMap::new(); + resources.insert(resource_name.clone(), resource); + resources.get_mut(&resource_name).unwrap() + }; + + if !caveat.is_object() { + return Err(anyhow!("Caveat must be an object: {}", caveat)); + } + + if let Some(ability_vec) = resource.get_mut(&ability) { + ability_vec.push(caveat); + } else { + resource.insert(ability, vec![caveat]); + } + } + Capabilities::try_from(resources) + } +} + +impl TryFrom for Capabilities { + type Error = anyhow::Error; + + fn try_from(value: CapabilitiesImpl) -> Result { + for (resource, abilities) in value.iter() { + if abilities.is_empty() { + // [0.10.0/3.2.6.2](https://github.com/ucan-wg/spec#3262-abilities): + // One or more abilities MUST be given for each resource. + return Err(anyhow!("No abilities given for resource: {}", resource)); + } + } + Ok(Capabilities(value)) + } +} + +impl TryFrom<&Value> for Capabilities { + type Error = anyhow::Error; + + fn try_from(value: &Value) -> Result { + let map = value + .as_object() + .ok_or_else(|| anyhow!("Capabilities must be an object."))?; + let mut resources: CapabilitiesImpl = BTreeMap::new(); + + for (key, value) in map.iter() { + let resource = key.to_owned(); + let abilities_object = value + .as_object() + .ok_or_else(|| anyhow!("Abilities must be an object."))?; + + let abilities = { + let mut abilities: AbilitiesImpl = BTreeMap::new(); + for (key, value) in abilities_object.iter() { + let ability = key.to_owned(); + let mut caveats: Vec = vec![]; + + let array = value + .as_array() + .ok_or_else(|| anyhow!("Caveats must be defined as an array."))?; + for value in array.iter() { + if !value.is_object() { + return Err(anyhow!("Caveat must be an object: {}", value)); + } + caveats.push(value.to_owned()); + } + abilities.insert(ability, caveats); + } + abilities + }; + + resources.insert(resource, abilities); + } + + Capabilities::try_from(resources) + } +} diff --git a/rust/noosphere-ucan/src/capability/mod.rs b/rust/noosphere-ucan/src/capability/mod.rs new file mode 100644 index 000000000..cde4fe337 --- /dev/null +++ b/rust/noosphere-ucan/src/capability/mod.rs @@ -0,0 +1,9 @@ +pub mod proof; + +mod caveats; +mod data; +mod semantics; + +pub use caveats::*; +pub use data::*; +pub use semantics::*; diff --git a/rust/noosphere-ucan/src/capability/proof.rs b/rust/noosphere-ucan/src/capability/proof.rs new file mode 100644 index 000000000..4499b5860 --- /dev/null +++ b/rust/noosphere-ucan/src/capability/proof.rs @@ -0,0 +1,84 @@ +use super::{Ability, CapabilitySemantics, Scope}; +use anyhow::{anyhow, Result}; +use std::fmt; +use url::Url; + +#[derive(Ord, Eq, PartialEq, PartialOrd, Clone)] +pub enum ProofAction { + Delegate, +} + +impl Ability for ProofAction {} + +impl TryFrom for ProofAction { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + match value.as_str() { + "ucan/DELEGATE" => Ok(ProofAction::Delegate), + unsupported => Err(anyhow!( + "Unsupported action for proof resource ({})", + unsupported + )), + } + } +} + +impl fmt::Display for ProofAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + ProofAction::Delegate => "ucan/DELEGATE", + } + ) + } +} + +#[derive(Eq, PartialEq, Clone)] +pub enum ProofSelection { + Index(usize), + All, +} + +impl Scope for ProofSelection { + fn contains(&self, other: &Self) -> bool { + self == other || *self == ProofSelection::All + } +} + +impl TryFrom for ProofSelection { + type Error = anyhow::Error; + + fn try_from(value: Url) -> Result { + match value.scheme() { + "prf" => String::from(value.path()).try_into(), + _ => Err(anyhow!("Unrecognized URI scheme")), + } + } +} + +impl TryFrom for ProofSelection { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + match value.as_str() { + "*" => Ok(ProofSelection::All), + selection => Ok(ProofSelection::Index(selection.parse::()?)), + } + } +} + +impl fmt::Display for ProofSelection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProofSelection::Index(usize) => write!(f, "prf:{}", usize), + ProofSelection::All => write!(f, "prf:*"), + } + } +} + +pub struct ProofDelegationSemantics {} + +impl CapabilitySemantics for ProofDelegationSemantics {} diff --git a/rust/noosphere-ucan/src/capability/semantics.rs b/rust/noosphere-ucan/src/capability/semantics.rs new file mode 100644 index 000000000..9c8db3c25 --- /dev/null +++ b/rust/noosphere-ucan/src/capability/semantics.rs @@ -0,0 +1,301 @@ +use super::{Capability, Caveat}; +use serde_json::{json, Value}; +use std::fmt::{self, Debug}; +use url::Url; + +pub trait Scope: ToString + TryFrom + PartialEq + Clone { + fn contains(&self, other: &Self) -> bool; +} + +pub trait Ability: Ord + TryFrom + ToString + Clone {} + +#[derive(Clone, Eq, PartialEq)] +pub enum ResourceUri +where + S: Scope, +{ + Scoped(S), + Unscoped, +} + +impl ResourceUri +where + S: Scope, +{ + pub fn contains(&self, other: &Self) -> bool { + match self { + ResourceUri::Unscoped => true, + ResourceUri::Scoped(scope) => match other { + ResourceUri::Scoped(other_scope) => scope.contains(other_scope), + _ => false, + }, + } + } +} + +impl fmt::Display for ResourceUri +where + S: Scope, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResourceUri::Unscoped => write!(f, "*"), + ResourceUri::Scoped(value) => write!(f, "{}", value.to_string()), + } + } +} + +#[derive(Clone, Eq, PartialEq)] +pub enum Resource +where + S: Scope, +{ + Resource { kind: ResourceUri }, + My { kind: ResourceUri }, + As { did: String, kind: ResourceUri }, +} + +impl Resource +where + S: Scope, +{ + pub fn contains(&self, other: &Self) -> bool { + match (self, other) { + ( + Resource::Resource { kind: resource }, + Resource::Resource { + kind: other_resource, + }, + ) => resource.contains(other_resource), + ( + Resource::My { kind: resource }, + Resource::My { + kind: other_resource, + }, + ) => resource.contains(other_resource), + ( + Resource::As { + did, + kind: resource, + }, + Resource::As { + did: other_did, + kind: other_resource, + }, + ) if did == other_did => resource.contains(other_resource), + _ => false, + } + } +} + +impl fmt::Display for Resource +where + S: Scope, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Resource::Resource { kind } => write!(f, "{}", kind), + Resource::My { kind } => write!(f, "my:{}", kind), + Resource::As { did, kind } => write!(f, "as:{}:{}", did, kind), + } + } +} + +pub trait CapabilitySemantics +where + S: Scope, + A: Ability, +{ + fn parse_scope(&self, scope: &Url) -> Option { + S::try_from(scope.clone()).ok() + } + fn parse_action(&self, ability: &str) -> Option { + A::try_from(String::from(ability)).ok() + } + + fn extract_did(&self, path: &str) -> Option<(String, String)> { + let mut path_parts = path.split(':'); + + match path_parts.next() { + Some("did") => (), + _ => return None, + }; + + match path_parts.next() { + Some("key") => (), + _ => return None, + }; + + let value = match path_parts.next() { + Some(value) => value, + _ => return None, + }; + + Some((format!("did:key:{value}"), path_parts.collect())) + } + + fn parse_resource(&self, resource: &Url) -> Option> { + Some(match resource.path() { + "*" => ResourceUri::Unscoped, + _ => ResourceUri::Scoped(self.parse_scope(resource)?), + }) + } + + fn parse_caveat(&self, caveat: Option<&Value>) -> Value { + if let Some(caveat) = caveat { + caveat.to_owned() + } else { + json!({}) + } + } + + /// Parse a resource and abilities string and a caveats object. + /// The default "no caveats" (`[{}]`) is implied if `None` caveats given. + fn parse( + &self, + resource: &str, + ability: &str, + caveat: Option<&Value>, + ) -> Option> { + let uri = Url::parse(resource).ok()?; + + let cap_resource = match uri.scheme() { + "my" => Resource::My { + kind: self.parse_resource(&uri)?, + }, + "as" => { + let (did, resource) = self.extract_did(uri.path())?; + Resource::As { + did, + kind: self.parse_resource(&Url::parse(resource.as_str()).ok()?)?, + } + } + _ => Resource::Resource { + kind: self.parse_resource(&uri)?, + }, + }; + + let cap_ability = match self.parse_action(ability) { + Some(ability) => ability, + None => return None, + }; + + let cap_caveat = self.parse_caveat(caveat); + + Some(CapabilityView::new_with_caveat( + cap_resource, + cap_ability, + cap_caveat, + )) + } + + fn parse_capability(&self, value: &Capability) -> Option> { + self.parse(&value.resource, &value.ability, Some(&value.caveat)) + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct CapabilityView +where + S: Scope, + A: Ability, +{ + pub resource: Resource, + pub ability: A, + pub caveat: Value, +} + +impl Debug for CapabilityView +where + S: Scope, + A: Ability, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Capability") + .field("resource", &self.resource.to_string()) + .field("ability", &self.ability.to_string()) + .field("caveats", &serde_json::to_string(&self.caveat)) + .finish() + } +} + +impl CapabilityView +where + S: Scope, + A: Ability, +{ + /// Creates a new [CapabilityView] semantics view over a capability + /// without caveats. + pub fn new(resource: Resource, ability: A) -> Self { + CapabilityView { + resource, + ability, + caveat: json!({}), + } + } + + /// Creates a new [CapabilityView] semantics view over a capability + /// with caveats. Note that an empty caveats array will imply NO + /// capabilities, rendering this capability meaningless. + pub fn new_with_caveat(resource: Resource, ability: A, caveat: Value) -> Self { + CapabilityView { + resource, + ability, + caveat, + } + } + + pub fn enables(&self, other: &CapabilityView) -> bool { + match ( + Caveat::try_from(self.caveat()), + Caveat::try_from(other.caveat()), + ) { + (Ok(self_caveat), Ok(other_caveat)) => { + self.resource.contains(&other.resource) + && self.ability >= other.ability + && self_caveat.enables(&other_caveat) + } + _ => false, + } + } + + pub fn resource(&self) -> &Resource { + &self.resource + } + + pub fn ability(&self) -> &A { + &self.ability + } + + pub fn caveat(&self) -> &Value { + &self.caveat + } +} + +impl From<&CapabilityView> for Capability +where + S: Scope, + A: Ability, +{ + fn from(value: &CapabilityView) -> Self { + Capability::new( + value.resource.to_string(), + value.ability.to_string(), + value.caveat.to_owned(), + ) + } +} + +impl From> for Capability +where + S: Scope, + A: Ability, +{ + fn from(value: CapabilityView) -> Self { + Capability::new( + value.resource.to_string(), + value.ability.to_string(), + value.caveat, + ) + } +} diff --git a/rust/noosphere-ucan/src/chain.rs b/rust/noosphere-ucan/src/chain.rs new file mode 100644 index 000000000..285efbba0 --- /dev/null +++ b/rust/noosphere-ucan/src/chain.rs @@ -0,0 +1,289 @@ +use crate::{ + capability::{ + proof::{ProofDelegationSemantics, ProofSelection}, + Ability, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, + }, + crypto::did::DidParser, + store::UcanJwtStore, + ucan::Ucan, +}; +use anyhow::{anyhow, Result}; +use async_recursion::async_recursion; +use cid::Cid; +use std::{collections::BTreeSet, fmt::Debug}; + +const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSemantics {}; + +#[derive(Eq, PartialEq)] +pub struct CapabilityInfo { + pub originators: BTreeSet, + pub not_before: Option, + pub expires_at: Option, + pub capability: CapabilityView, +} + +impl Debug for CapabilityInfo +where + S: Scope, + A: Ability, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CapabilityInfo") + .field("originators", &self.originators) + .field("not_before", &self.not_before) + .field("expires_at", &self.expires_at) + .field("capability", &self.capability) + .finish() + } +} + +/// A deserialized chain of ancestral proofs that are linked to a UCAN +#[derive(Debug)] +pub struct ProofChain { + ucan: Ucan, + proofs: Vec, + redelegations: BTreeSet, +} + +impl ProofChain { + /// Instantiate a [ProofChain] from a [Ucan], given a [UcanJwtStore] and [DidParser] + #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] + #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] + pub async fn from_ucan( + ucan: Ucan, + now_time: Option, + did_parser: &mut DidParser, + store: &S, + ) -> Result + where + S: UcanJwtStore, + { + ucan.validate(now_time, did_parser).await?; + + let mut proofs: Vec = Vec::new(); + + if let Some(ucan_proofs) = ucan.proofs() { + for cid_string in ucan_proofs.iter() { + let cid = Cid::try_from(cid_string.as_str())?; + let ucan_token = store.require_token(&cid).await?; + let proof_chain = + Self::try_from_token_string(&ucan_token, now_time, did_parser, store).await?; + proof_chain.validate_link_to(&ucan)?; + proofs.push(proof_chain); + } + } + + let mut redelegations = BTreeSet::::new(); + + for capability in ucan + .capabilities() + .iter() + .filter_map(|cap| PROOF_DELEGATION_SEMANTICS.parse_capability(&cap)) + { + match capability.resource() { + Resource::Resource { + kind: ResourceUri::Scoped(ProofSelection::All), + } => { + for index in 0..proofs.len() { + redelegations.insert(index); + } + } + Resource::Resource { + kind: ResourceUri::Scoped(ProofSelection::Index(index)), + } => { + if *index < proofs.len() { + redelegations.insert(*index); + } else { + return Err(anyhow!( + "Unable to redelegate proof; no proof at zero-based index {}", + index + )); + } + } + _ => continue, + } + } + + Ok(ProofChain { + ucan, + proofs, + redelegations, + }) + } + + /// Instantiate a [ProofChain] from a [Cid], given a [UcanJwtStore] and [DidParser] + /// The [Cid] must resolve to a JWT token string + pub async fn from_cid( + cid: &Cid, + now_time: Option, + did_parser: &mut DidParser, + store: &S, + ) -> Result + where + S: UcanJwtStore, + { + Self::try_from_token_string( + &store.require_token(cid).await?, + now_time, + did_parser, + store, + ) + .await + } + + /// Instantiate a [ProofChain] from a JWT token string, given a [UcanJwtStore] and [DidParser] + pub async fn try_from_token_string<'a, S>( + ucan_token_string: &str, + now_time: Option, + did_parser: &mut DidParser, + store: &S, + ) -> Result + where + S: UcanJwtStore, + { + let ucan = Ucan::try_from(ucan_token_string)?; + Self::from_ucan(ucan, now_time, did_parser, store).await + } + + fn validate_link_to(&self, ucan: &Ucan) -> Result<()> { + let audience = self.ucan.audience(); + let issuer = ucan.issuer(); + + match audience == issuer { + true => match self.ucan.lifetime_encompasses(ucan) { + true => Ok(()), + false => Err(anyhow!("Invalid UCAN link: lifetime exceeds attenuation")), + }, + false => Err(anyhow!( + "Invalid UCAN link: audience {} does not match issuer {}", + audience, + issuer + )), + } + } + + pub fn ucan(&self) -> &Ucan { + &self.ucan + } + + pub fn proofs(&self) -> &Vec { + &self.proofs + } + + pub fn reduce_capabilities( + &self, + semantics: &Semantics, + ) -> Vec> + where + Semantics: CapabilitySemantics, + S: Scope, + A: Ability, + { + // Get the set of inherited attenuations (excluding redelegations) + // before further attenuating by own lifetime and capabilities: + let ancestral_capability_infos: Vec> = self + .proofs + .iter() + .enumerate() + .flat_map(|(index, ancestor_chain)| { + if self.redelegations.contains(&index) { + Vec::new() + } else { + ancestor_chain.reduce_capabilities(semantics) + } + }) + .collect(); + + // Get the set of capabilities that are blanket redelegated from + // ancestor proofs (via the prf: resource): + let mut redelegated_capability_infos: Vec> = self + .redelegations + .iter() + .flat_map(|index| { + self.proofs + .get(*index) + .unwrap() + .reduce_capabilities(semantics) + .into_iter() + .map(|mut info| { + // Redelegated capabilities should be attenuated by + // this UCAN's lifetime + info.not_before = *self.ucan.not_before(); + info.expires_at = *self.ucan.expires_at(); + info + }) + }) + .collect(); + + let self_capabilities_iter = self + .ucan + .capabilities() + .iter() + .map_while(|data| semantics.parse_capability(&data)); + + // Get the claimed attenuations of this ucan, cross-checking ancestral + // attenuations to discover the originating authority + let mut self_capability_infos: Vec> = match self.proofs.len() { + 0 => self_capabilities_iter + .map(|capability| CapabilityInfo { + originators: BTreeSet::from_iter(vec![self.ucan.issuer().to_string()]), + capability, + not_before: *self.ucan.not_before(), + expires_at: *self.ucan.expires_at(), + }) + .collect(), + _ => self_capabilities_iter + .map(|capability| { + let mut originators = BTreeSet::::new(); + + for ancestral_capability_info in ancestral_capability_infos.iter() { + match ancestral_capability_info.capability.enables(&capability) { + true => { + originators.extend(ancestral_capability_info.originators.clone()) + } + // true => return Some(capability), + false => continue, + } + } + + // If there are no related ancestral capability, then this + // link in the chain is considered the first originator + if originators.is_empty() { + originators.insert(self.ucan.issuer().to_string()); + } + + CapabilityInfo { + capability, + originators, + not_before: *self.ucan.not_before(), + expires_at: *self.ucan.expires_at(), + } + }) + .collect(), + }; + + self_capability_infos.append(&mut redelegated_capability_infos); + + let mut merged_capability_infos = Vec::>::new(); + + // Merge redundant capabilities (accounting for redelegation), ensuring + // that discrete originators are aggregated as we go + 'merge: while let Some(capability_info) = self_capability_infos.pop() { + for remaining_capability_info in &mut self_capability_infos { + if remaining_capability_info + .capability + .enables(&capability_info.capability) + { + remaining_capability_info + .originators + .extend(capability_info.originators); + continue 'merge; + } + } + + merged_capability_infos.push(capability_info); + } + + merged_capability_infos + } +} diff --git a/rust/noosphere-ucan/src/crypto/did.rs b/rust/noosphere-ucan/src/crypto/did.rs new file mode 100644 index 000000000..fe7622d64 --- /dev/null +++ b/rust/noosphere-ucan/src/crypto/did.rs @@ -0,0 +1,67 @@ +use super::KeyMaterial; +use anyhow::{anyhow, Result}; +use std::{collections::BTreeMap, sync::Arc}; + +pub type DidPrefix = &'static [u8]; +pub type BytesToKey = fn(Vec) -> Result>; +pub type KeyConstructors = BTreeMap; +pub type KeyConstructorSlice = [(DidPrefix, BytesToKey)]; +pub type KeyCache = BTreeMap>>; + +pub const DID_PREFIX: &str = "did:"; +pub const DID_KEY_PREFIX: &str = "did:key:z"; + +pub const ED25519_MAGIC_BYTES: &[u8] = &[0xed, 0x01]; +pub const RSA_MAGIC_BYTES: &[u8] = &[0x85, 0x24]; +pub const BLS12381G1_MAGIC_BYTES: &[u8] = &[0xea, 0x01]; +pub const BLS12381G2_MAGIC_BYTES: &[u8] = &[0xeb, 0x01]; +pub const P256_MAGIC_BYTES: &[u8] = &[0x80, 0x24]; +pub const SECP256K1_MAGIC_BYTES: &[u8] = &[0xe7, 0x1]; + +/// A parser that is able to convert from a DID string into a corresponding +/// [`KeyMaterial`] implementation. The parser extracts the signature +/// magic bytes from a given DID and tries to match them to a corresponding +/// constructor function that produces a `SigningKey`. +pub struct DidParser { + key_constructors: KeyConstructors, + key_cache: KeyCache, +} + +impl DidParser { + pub fn new(key_constructor_slice: &KeyConstructorSlice) -> Self { + let mut key_constructors = BTreeMap::new(); + for pair in key_constructor_slice { + key_constructors.insert(pair.0, pair.1); + } + DidParser { + key_constructors, + key_cache: BTreeMap::new(), + } + } + + pub fn parse(&mut self, did: &str) -> Result>> { + if !did.starts_with(DID_KEY_PREFIX) { + return Err(anyhow!("Expected valid did:key, got: {}", did)); + } + + let did = did.to_owned(); + if let Some(key) = self.key_cache.get(&did) { + return Ok(key.clone()); + } + + let did_bytes = bs58::decode(&did[DID_KEY_PREFIX.len()..]).into_vec()?; + let magic_bytes = &did_bytes[0..2]; + match self.key_constructors.get(magic_bytes) { + Some(ctor) => { + let key = ctor(Vec::from(&did_bytes[2..]))?; + self.key_cache.insert(did.clone(), Arc::new(key)); + + self.key_cache + .get(&did) + .ok_or_else(|| anyhow!("Couldn't find cached key")) + .cloned() + } + None => Err(anyhow!("Unrecognized magic bytes: {:?}", magic_bytes)), + } + } +} diff --git a/rust/noosphere-ucan/src/crypto/key.rs b/rust/noosphere-ucan/src/crypto/key.rs new file mode 100644 index 000000000..022176ac0 --- /dev/null +++ b/rust/noosphere-ucan/src/crypto/key.rs @@ -0,0 +1,79 @@ +use anyhow::Result; +use async_trait::async_trait; +use std::sync::Arc; + +#[cfg(not(target_arch = "wasm32"))] +pub trait KeyMaterialConditionalSendSync: Send + Sync {} + +#[cfg(not(target_arch = "wasm32"))] +impl KeyMaterialConditionalSendSync for K where K: KeyMaterial + Send + Sync {} + +#[cfg(target_arch = "wasm32")] +pub trait KeyMaterialConditionalSendSync {} + +#[cfg(target_arch = "wasm32")] +impl KeyMaterialConditionalSendSync for K where K: KeyMaterial {} + +/// This trait must be implemented by a struct that encapsulates cryptographic +/// keypair data. The trait represent the minimum required API capability for +/// producing a signed UCAN from a cryptographic keypair, and verifying such +/// signatures. +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait KeyMaterial: KeyMaterialConditionalSendSync { + /// The algorithm that will be used to produce the signature returned by the + /// sign method in this implementation + fn get_jwt_algorithm_name(&self) -> String; + + /// Provides a valid DID that can be used to solve the key + async fn get_did(&self) -> Result; + + /// Sign some data with this key + async fn sign(&self, payload: &[u8]) -> Result>; + + /// Verify the alleged signature of some data against this key + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl KeyMaterial for Box { + fn get_jwt_algorithm_name(&self) -> String { + self.as_ref().get_jwt_algorithm_name() + } + + async fn get_did(&self) -> Result { + self.as_ref().get_did().await + } + + async fn sign(&self, payload: &[u8]) -> Result> { + self.as_ref().sign(payload).await + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + self.as_ref().verify(payload, signature).await + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl KeyMaterial for Arc +where + K: KeyMaterial, +{ + fn get_jwt_algorithm_name(&self) -> String { + (**self).get_jwt_algorithm_name() + } + + async fn get_did(&self) -> Result { + (**self).get_did().await + } + + async fn sign(&self, payload: &[u8]) -> Result> { + (**self).sign(payload).await + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + (**self).verify(payload, signature).await + } +} diff --git a/rust/noosphere-ucan/src/crypto/mod.rs b/rust/noosphere-ucan/src/crypto/mod.rs new file mode 100644 index 000000000..75ac72f08 --- /dev/null +++ b/rust/noosphere-ucan/src/crypto/mod.rs @@ -0,0 +1,6 @@ +pub mod did; +mod key; +mod signature; + +pub use key::*; +pub use signature::*; diff --git a/rust/noosphere-ucan/src/crypto/signature.rs b/rust/noosphere-ucan/src/crypto/signature.rs new file mode 100644 index 000000000..bc319d66c --- /dev/null +++ b/rust/noosphere-ucan/src/crypto/signature.rs @@ -0,0 +1,12 @@ +use strum_macros::{Display, EnumString}; + +// See: https://www.rfc-editor.org/rfc/rfc7518 +// See: https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.4 +#[derive(Debug, Display, EnumString, Eq, PartialEq)] +pub enum JwtSignatureAlgorithm { + EdDSA, + RS256, + ES256, + ES384, + ES512, +} diff --git a/rust/noosphere-ucan/src/ipld/mod.rs b/rust/noosphere-ucan/src/ipld/mod.rs new file mode 100644 index 000000000..d055d81fd --- /dev/null +++ b/rust/noosphere-ucan/src/ipld/mod.rs @@ -0,0 +1,7 @@ +mod principle; +mod signature; +mod ucan; + +pub use self::ucan::*; +pub use principle::*; +pub use signature::*; diff --git a/rust/noosphere-ucan/src/ipld/principle.rs b/rust/noosphere-ucan/src/ipld/principle.rs new file mode 100644 index 000000000..9980f40fb --- /dev/null +++ b/rust/noosphere-ucan/src/ipld/principle.rs @@ -0,0 +1,66 @@ +use crate::crypto::did::{DID_KEY_PREFIX, DID_PREFIX}; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, str::FromStr}; + +// Note: varint encoding of 0x0d1d +pub const DID_IPLD_PREFIX: &[u8] = &[0x9d, 0x1a]; + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Principle(Vec); + +impl FromStr for Principle { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + if let Some(stripped) = input.strip_prefix(DID_KEY_PREFIX) { + Ok(Principle(bs58::decode(stripped).into_vec()?)) + } else if let Some(stripped) = input.strip_prefix(DID_PREFIX) { + Ok(Principle([DID_IPLD_PREFIX, stripped.as_bytes()].concat())) + } else { + Err(anyhow!("This is not a DID: {}", input)) + } + } +} + +impl Display for Principle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = &self.0; + let did_content = match &bytes[0..2] { + DID_IPLD_PREFIX => [ + DID_PREFIX, + std::str::from_utf8(&bytes[2..]).map_err(|_| std::fmt::Error)?, + ] + .concat(), + _ => [DID_KEY_PREFIX, &bs58::encode(bytes).into_string()].concat(), + }; + + write!(f, "{did_content}") + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::{ipld::Principle, tests::helpers::dag_cbor_roundtrip}; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn it_round_trips_a_principle_did() { + let did_string = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + let principle = dag_cbor_roundtrip(&Principle::from_str(&did_string).unwrap()).unwrap(); + assert_eq!(did_string, principle.to_string()); + + let did_string = "did:web:example.com"; + let principle = dag_cbor_roundtrip(&Principle::from_str(&did_string).unwrap()).unwrap(); + assert_eq!(did_string, principle.to_string()); + } +} diff --git a/rust/noosphere-ucan/src/ipld/signature.rs b/rust/noosphere-ucan/src/ipld/signature.rs new file mode 100644 index 000000000..084cae0cf --- /dev/null +++ b/rust/noosphere-ucan/src/ipld/signature.rs @@ -0,0 +1,195 @@ +use crate::crypto::JwtSignatureAlgorithm; +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +// See +// See +const NONSTANDARD_VARSIG_PREFIX: u64 = 0xd000; +const ES256K_VARSIG_PREFIX: u64 = 0xd0e7; +const BLS12381G1_VARSIG_PREFIX: u64 = 0xd0ea; +const BLS12381G2_VARSIG_PREFIX: u64 = 0xd0eb; +const EDDSA_VARSIG_PREFIX: u64 = 0xd0ed; +const ES256_VARSIG_PREFIX: u64 = 0xd01200; +const ES384_VARSIG_PREFIX: u64 = 0xd01201; +const ES512_VARSIG_PREFIX: u64 = 0xd01202; +const RS256_VARSIG_PREFIX: u64 = 0xd01205; +const EIP191_VARSIG_PREFIX: u64 = 0xd191; + +/// A helper for transforming signatures used in JWTs to their UCAN-IPLD +/// counterpart representation and vice-versa +/// Note, not all valid JWT signature algorithms are represented by this +/// library, nor are all valid varsig prefixes +/// See +#[derive(Debug, Eq, PartialEq)] +pub enum VarsigPrefix { + NonStandard, + ES256K, + BLS12381G1, + BLS12381G2, + EdDSA, + ES256, + ES384, + ES512, + RS256, + EIP191, +} + +impl TryFrom for VarsigPrefix { + type Error = anyhow::Error; + + fn try_from(value: JwtSignatureAlgorithm) -> Result { + Ok(match value { + JwtSignatureAlgorithm::EdDSA => VarsigPrefix::EdDSA, + JwtSignatureAlgorithm::RS256 => VarsigPrefix::RS256, + JwtSignatureAlgorithm::ES256 => VarsigPrefix::ES256, + JwtSignatureAlgorithm::ES384 => VarsigPrefix::ES384, + JwtSignatureAlgorithm::ES512 => VarsigPrefix::ES512, + }) + } +} + +impl TryFrom for JwtSignatureAlgorithm { + type Error = anyhow::Error; + + fn try_from(value: VarsigPrefix) -> Result { + Ok(match value { + VarsigPrefix::EdDSA => JwtSignatureAlgorithm::EdDSA, + VarsigPrefix::RS256 => JwtSignatureAlgorithm::RS256, + VarsigPrefix::ES256 => JwtSignatureAlgorithm::ES256, + VarsigPrefix::ES384 => JwtSignatureAlgorithm::ES384, + VarsigPrefix::ES512 => JwtSignatureAlgorithm::ES512, + _ => { + return Err(anyhow!( + "JWT signature algorithm name for {:?} is not known", + value + )) + } + }) + } +} + +impl FromStr for VarsigPrefix { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + VarsigPrefix::try_from(JwtSignatureAlgorithm::from_str(s)?) + } +} + +impl From for u64 { + fn from(value: VarsigPrefix) -> Self { + match value { + VarsigPrefix::NonStandard { .. } => NONSTANDARD_VARSIG_PREFIX, + VarsigPrefix::ES256K => ES256K_VARSIG_PREFIX, + VarsigPrefix::BLS12381G1 => BLS12381G1_VARSIG_PREFIX, + VarsigPrefix::BLS12381G2 => BLS12381G2_VARSIG_PREFIX, + VarsigPrefix::EdDSA => EDDSA_VARSIG_PREFIX, + VarsigPrefix::ES256 => ES256_VARSIG_PREFIX, + VarsigPrefix::ES384 => ES384_VARSIG_PREFIX, + VarsigPrefix::ES512 => ES512_VARSIG_PREFIX, + VarsigPrefix::RS256 => RS256_VARSIG_PREFIX, + VarsigPrefix::EIP191 => EIP191_VARSIG_PREFIX, + } + } +} + +impl TryFrom for VarsigPrefix { + type Error = anyhow::Error; + + fn try_from(value: u64) -> Result { + Ok(match value { + EDDSA_VARSIG_PREFIX => VarsigPrefix::EdDSA, + RS256_VARSIG_PREFIX => VarsigPrefix::RS256, + ES256K_VARSIG_PREFIX => VarsigPrefix::ES256K, + BLS12381G1_VARSIG_PREFIX => VarsigPrefix::BLS12381G1, + BLS12381G2_VARSIG_PREFIX => VarsigPrefix::BLS12381G2, + EIP191_VARSIG_PREFIX => VarsigPrefix::EIP191, + ES256_VARSIG_PREFIX => VarsigPrefix::ES256, + ES384_VARSIG_PREFIX => VarsigPrefix::ES384, + ES512_VARSIG_PREFIX => VarsigPrefix::ES512, + NONSTANDARD_VARSIG_PREFIX => VarsigPrefix::NonStandard, + _ => return Err(anyhow!("Signature does not have a recognized prefix")), + }) + } +} + +/// An envelope for the UCAN-IPLD-equivalent of a UCAN's JWT signature, which +/// is a specified prefix in front of the raw signature bytes +/// See: +#[repr(transparent)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Signature(pub Vec); + +impl Signature { + pub fn decode(&self) -> Result<(JwtSignatureAlgorithm, Vec)> { + let buffer = self.0.as_slice(); + let (prefix, buffer) = + unsigned_varint::decode::u64(buffer).map_err(|e| anyhow!("{}", e))?; + let (signature_length, buffer) = + unsigned_varint::decode::usize(buffer).map_err(|e| anyhow!("{}", e))?; + + // TODO: Non-standard algorithm support here... + + let algorithm = JwtSignatureAlgorithm::try_from(VarsigPrefix::try_from(prefix)?)?; + let signature = buffer[..signature_length].to_vec(); + + Ok((algorithm, signature)) + } +} + +// TODO: Support non-standard signature algorithms for experimental purposes +// Note that non-standard signatures should additionally have the signature name +// appended after the signature bytes in the varsig representation +impl> TryFrom<(JwtSignatureAlgorithm, T)> for Signature { + type Error = anyhow::Error; + + fn try_from((algorithm, signature): (JwtSignatureAlgorithm, T)) -> Result { + // TODO: Non-standard JWT algorithm support here + let signature_bytes = signature.as_ref(); + let prefix = VarsigPrefix::try_from(algorithm)?; + let mut prefix_buffer = unsigned_varint::encode::u64_buffer(); + let prefix_bytes = unsigned_varint::encode::u64(prefix.into(), &mut prefix_buffer); + let mut size_buffer = unsigned_varint::encode::usize_buffer(); + + let size_bytes = unsigned_varint::encode::usize(signature_bytes.len(), &mut size_buffer); + + Ok(Signature( + [prefix_bytes, size_bytes, signature_bytes].concat(), + )) + } +} + +#[cfg(test)] +mod tests { + use crate::{crypto::JwtSignatureAlgorithm, ipld::Signature}; + + use base64::Engine; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn it_can_convert_between_jwt_and_bytesprefix_form() { + let token_signature = "Ab-xfYRoqYEHuo-252MKXDSiOZkLD-h1gHt8gKBP0AVdJZ6Jruv49TLZOvgWy9QkCpiwKUeGVbHodKcVx-azCQ"; + let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(token_signature) + .unwrap(); + + let bytesprefix_signature = + Signature::try_from((JwtSignatureAlgorithm::EdDSA, &signature_bytes)).unwrap(); + + let (decoded_algorithm, decoded_signature_bytes) = bytesprefix_signature.decode().unwrap(); + + assert_eq!(decoded_algorithm, JwtSignatureAlgorithm::EdDSA); + assert_eq!(decoded_signature_bytes, signature_bytes); + } + + #[allow(dead_code)] + // #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[ignore = "Support non-standard signature algorithms"] + fn it_can_convert_between_jwt_and_bytesprefix_for_nonstandard_signatures() {} +} diff --git a/rust/noosphere-ucan/src/ipld/ucan.rs b/rust/noosphere-ucan/src/ipld/ucan.rs new file mode 100644 index 000000000..5efcd97a4 --- /dev/null +++ b/rust/noosphere-ucan/src/ipld/ucan.rs @@ -0,0 +1,201 @@ +use crate::{ + capability::Capabilities, + crypto::JwtSignatureAlgorithm, + ipld::{Principle, Signature}, + serde::Base64Encode, + ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, +}; +use cid::Cid; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UcanIpld { + pub v: String, + + pub iss: Principle, + pub aud: Principle, + pub s: Signature, + + pub cap: Capabilities, + pub prf: Option>, + pub exp: Option, + pub fct: Option, + + pub nnc: Option, + pub nbf: Option, +} + +impl TryFrom<&Ucan> for UcanIpld { + type Error = anyhow::Error; + + fn try_from(ucan: &Ucan) -> Result { + let prf = if let Some(proofs) = ucan.proofs() { + let mut prf = Vec::new(); + for cid_string in proofs { + prf.push(Cid::try_from(cid_string.as_str())?); + } + if prf.is_empty() { + None + } else { + Some(prf) + } + } else { + None + }; + + Ok(UcanIpld { + v: ucan.version().to_string(), + iss: Principle::from_str(ucan.issuer())?, + aud: Principle::from_str(ucan.audience())?, + s: Signature::try_from(( + JwtSignatureAlgorithm::from_str(ucan.algorithm())?, + ucan.signature(), + ))?, + cap: ucan.capabilities().clone(), + prf, + exp: *ucan.expires_at(), + fct: ucan.facts().clone(), + nnc: ucan.nonce().as_ref().cloned(), + nbf: *ucan.not_before(), + }) + } +} + +impl TryFrom<&UcanIpld> for Ucan { + type Error = anyhow::Error; + + fn try_from(value: &UcanIpld) -> Result { + let (algorithm, signature) = value.s.decode()?; + + let header = UcanHeader { + alg: algorithm.to_string(), + typ: "JWT".into(), + }; + + let payload = UcanPayload { + ucv: UCAN_VERSION.into(), + iss: value.iss.to_string(), + aud: value.aud.to_string(), + exp: value.exp, + nbf: value.nbf, + nnc: value.nnc.clone(), + cap: value.cap.clone(), + fct: value.fct.clone(), + prf: value + .prf + .clone() + .map(|prf| prf.iter().map(|cid| cid.to_string()).collect()), + }; + + let signed_data = format!( + "{}.{}", + header.jwt_base64_encode()?, + payload.jwt_base64_encode()? + ) + .as_bytes() + .to_vec(); + + Ok(Ucan::new(header, payload, signed_data, signature)) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::json; + + use crate::{ + tests::{ + fixtures::Identities, + helpers::{dag_cbor_roundtrip, scaffold_ucan_builder}, + }, + Ucan, + }; + + use super::UcanIpld; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_produces_canonical_jwt_despite_json_ambiguity() { + let identities = Identities::new().await; + let canon_builder = scaffold_ucan_builder(&identities).await.unwrap(); + let other_builder = scaffold_ucan_builder(&identities).await.unwrap(); + + let canon_jwt = canon_builder + .with_fact( + "abc/challenge", + json!({ + "baz": true, + "foo": "bar" + }), + ) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let other_jwt = other_builder + .with_fact( + "abc/challenge", + json!({ + "foo": "bar", + "baz": true + }), + ) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + assert_eq!(canon_jwt, other_jwt); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_stays_canonical_when_converting_between_jwt_and_ipld() { + let identities = Identities::new().await; + let builder = scaffold_ucan_builder(&identities).await.unwrap(); + + let jwt = builder + .with_fact( + "abc/challenge", + json!({ + "baz": true, + "foo": "bar" + }), + ) + .with_nonce() + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let ucan = Ucan::try_from(jwt.as_str()).unwrap(); + let ucan_ipld = UcanIpld::try_from(&ucan).unwrap(); + + let decoded_ucan_ipld = dag_cbor_roundtrip(&ucan_ipld).unwrap(); + + let decoded_ucan = Ucan::try_from(&decoded_ucan_ipld).unwrap(); + + let decoded_jwt = decoded_ucan.encode().unwrap(); + + assert_eq!(jwt, decoded_jwt); + } +} diff --git a/rust/noosphere-ucan/src/lib.rs b/rust/noosphere-ucan/src/lib.rs new file mode 100644 index 000000000..113210923 --- /dev/null +++ b/rust/noosphere-ucan/src/lib.rs @@ -0,0 +1,92 @@ +//! Implement UCAN-based authorization with conciseness and ease! +//! +//! [UCANs][UCAN docs] are an emerging pattern based on +//! [JSON Web Tokens][JWT docs] (aka JWTs) that facilitate distributed and/or +//! decentralized authorization flows in web applications. Visit +//! [https://ucan.xyz][UCAN docs] for an introduction to UCANs and ideas for +//! how you can use them in your application. +//! +//! # Examples +//! +//! This crate offers the [`builder::UcanBuilder`] abstraction to generate +//! signed UCAN tokens. +//! +//! To generate a signed token, you need to provide a [`crypto::KeyMaterial`] +//! implementation. For more information on providing a signing key, see the +//! [`crypto`] module documentation. +//! +//! ```rust +//! use noosphere_ucan::{ +//! builder::UcanBuilder, +//! crypto::KeyMaterial, +//! }; +//! +//! async fn generate_token<'a, K: KeyMaterial>(issuer_key: &'a K, audience_did: &'a str) -> Result { +//! UcanBuilder::default() +//! .issued_by(issuer_key) +//! .for_audience(audience_did) +//! .with_lifetime(60) +//! .build()? +//! .sign().await? +//! .encode() +//! } +//! ``` +//! +//! The crate also offers a validating parser to interpret UCAN tokens and +//! the capabilities they grant via their issuer and/or witnessing proofs: +//! the [`chain::ProofChain`]. +//! +//! Most capabilities are closely tied to a specific application domain. See the +//! [`capability`] module documentation to read more about defining your own +//! domain-specific semantics. +//! +//! ```rust +//! use noosphere_ucan::{ +//! chain::{ProofChain, CapabilityInfo}, +//! capability::{CapabilitySemantics, Scope, Ability}, +//! crypto::did::{DidParser, KeyConstructorSlice}, +//! store::UcanJwtStore +//! }; +//! +//! const SUPPORTED_KEY_TYPES: &KeyConstructorSlice = &[ +//! // You must bring your own key support +//! ]; +//! +//! async fn get_capabilities<'a, Semantics, S, A, Store>(ucan_token: &'a str, semantics: &'a Semantics, store: &'a Store) -> Result>, anyhow::Error> +//! where +//! Semantics: CapabilitySemantics, +//! S: Scope, +//! A: Ability, +//! Store: UcanJwtStore +//! { +//! let mut did_parser = DidParser::new(SUPPORTED_KEY_TYPES); +//! +//! Ok(ProofChain::try_from_token_string(ucan_token, None, &mut did_parser, store).await? +//! .reduce_capabilities(semantics)) +//! } +//! ``` +//! +//! Note that you must bring your own key support in order to build a +//! `ProofChain`, via a [`crypto::did::DidParser`]. This is so that the core +//! library can remain agnostic of backing implementations for specific key +//! types. +//! +//! [JWT docs]: https://jwt.io/ +//! [UCAN docs]: https://ucan.xyz/ +//! [DID spec]: https://www.w3.org/TR/did-core/ +//! [DID Key spec]: https://w3c-ccg.github.io/did-method-key/ + +pub mod crypto; +pub mod time; + +pub mod builder; +pub mod capability; +pub mod chain; +pub mod ipld; +pub mod serde; +pub mod store; +pub mod ucan; +pub use self::ucan::Ucan; + +#[cfg(test)] +mod tests; diff --git a/rust/noosphere-ucan/src/serde.rs b/rust/noosphere-ucan/src/serde.rs new file mode 100644 index 000000000..a2ffebc43 --- /dev/null +++ b/rust/noosphere-ucan/src/serde.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use base64::Engine; +use libipld_core::{ + codec::{Decode, Encode}, + ipld::Ipld, + serde::{from_ipld, to_ipld}, +}; +use libipld_json::DagJsonCodec; +use serde::{de::DeserializeOwned, Serialize, Serializer}; +use std::io::Cursor; + +/// Utility function to enforce lower-case string values when serializing +pub fn ser_to_lower_case(string: &str, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&string.to_lowercase()) +} + +/// Helper trait to ser/de any serde-implementing value to/from DAG-JSON +pub trait DagJson: Serialize + DeserializeOwned { + fn to_dag_json(&self) -> Result> { + let ipld = to_ipld(self)?; + let mut json_bytes = Vec::new(); + + ipld.encode(DagJsonCodec, &mut json_bytes)?; + + Ok(json_bytes) + } + + fn from_dag_json(json_bytes: &[u8]) -> Result { + let ipld = Ipld::decode(DagJsonCodec, &mut Cursor::new(json_bytes))?; + Ok(from_ipld(ipld)?) + } +} + +impl DagJson for T where T: Serialize + DeserializeOwned {} + +/// Helper trait to encode structs as base64 as part of creating a JWT +pub trait Base64Encode: DagJson { + fn jwt_base64_encode(&self) -> Result { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.to_dag_json()?)) + } +} + +impl Base64Encode for T where T: DagJson {} diff --git a/rust/noosphere-ucan/src/store.rs b/rust/noosphere-ucan/src/store.rs new file mode 100644 index 000000000..034a4c325 --- /dev/null +++ b/rust/noosphere-ucan/src/store.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cid::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use libipld_core::{ + codec::{Codec, Decode, Encode}, + ipld::Ipld, + raw::RawCodec, +}; +use std::{ + collections::HashMap, + io::Cursor, + sync::{Arc, Mutex}, +}; + +#[cfg(not(target_arch = "wasm32"))] +pub trait UcanStoreConditionalSend: Send {} + +#[cfg(not(target_arch = "wasm32"))] +impl UcanStoreConditionalSend for U where U: Send {} + +#[cfg(target_arch = "wasm32")] +pub trait UcanStoreConditionalSend {} + +#[cfg(target_arch = "wasm32")] +impl UcanStoreConditionalSend for U {} + +#[cfg(not(target_arch = "wasm32"))] +pub trait UcanStoreConditionalSendSync: Send + Sync {} + +#[cfg(not(target_arch = "wasm32"))] +impl UcanStoreConditionalSendSync for U where U: Send + Sync {} + +#[cfg(target_arch = "wasm32")] +pub trait UcanStoreConditionalSendSync {} + +#[cfg(target_arch = "wasm32")] +impl UcanStoreConditionalSendSync for U {} + +/// This trait is meant to be implemented by a storage backend suitable for +/// persisting UCAN tokens that may be referenced as proofs by other UCANs +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait UcanStore: UcanStoreConditionalSendSync { + /// Read a value from the store by CID, returning a Result> that unwraps + /// to None if no value is found, otherwise Some + async fn read>(&self, cid: &Cid) -> Result>; + + /// Write a value to the store, receiving a Result that wraps the values CID if the + /// write was successful + async fn write + UcanStoreConditionalSend + core::fmt::Debug>( + &mut self, + token: T, + ) -> Result; +} + +/// This trait is sugar over the UcanStore trait to add convenience methods +/// for the case of storing JWT-encoded UCAN strings using the 'raw' codec +/// which is the only combination strictly required by the UCAN spec +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait UcanJwtStore: UcanStore { + async fn require_token(&self, cid: &Cid) -> Result { + match self.read_token(cid).await? { + Some(token) => Ok(token), + None => Err(anyhow!("No token found for CID {}", cid.to_string())), + } + } + + async fn read_token(&self, cid: &Cid) -> Result> { + let codec = RawCodec; + + if cid.codec() != u64::from(codec) { + return Err(anyhow!( + "Only 'raw' codec supported, but CID refers to {:#x}", + cid.codec() + )); + } + + match self.read::(cid).await? { + Some(Ipld::Bytes(bytes)) => Ok(Some(std::str::from_utf8(&bytes)?.to_string())), + _ => Err(anyhow!("No UCAN was found for CID {:?}", cid)), + } + } + + async fn write_token(&mut self, token: &str) -> Result { + self.write(Ipld::Bytes(token.as_bytes().to_vec())).await + } +} + +impl UcanJwtStore for U where U: UcanStore {} + +/// A basic in-memory store that implements UcanStore for the 'raw' +/// codec. This will serve for basic use cases and tests, but it is +/// recommended that a store that persists to disk be used in most +/// practical use cases. +#[derive(Clone, Default, Debug)] +pub struct MemoryStore { + dags: Arc>>>, +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl UcanStore for MemoryStore { + async fn read>(&self, cid: &Cid) -> Result> { + let codec = RawCodec; + let dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + + Ok(match dags.get(cid) { + Some(bytes) => Some(T::decode(codec, &mut Cursor::new(bytes))?), + None => None, + }) + } + + async fn write + UcanStoreConditionalSend + core::fmt::Debug>( + &mut self, + token: T, + ) -> Result { + let codec = RawCodec; + let block = codec.encode(&token)?; + let cid = Cid::new_v1(codec.into(), Code::Blake3_256.digest(&block)); + + let mut dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + dags.insert(cid, block); + + Ok(cid) + } +} diff --git a/rust/noosphere-ucan/src/tests/attenuation.rs b/rust/noosphere-ucan/src/tests/attenuation.rs new file mode 100644 index 000000000..2ef9679f4 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/attenuation.rs @@ -0,0 +1,447 @@ +use super::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}; +use crate::{ + builder::UcanBuilder, + capability::{Capability, CapabilitySemantics}, + chain::{CapabilityInfo, ProofChain}, + crypto::did::DidParser, + store::{MemoryStore, UcanJwtStore}, +}; +use std::collections::BTreeSet; + +use serde_json::json; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_works_with_a_simple_example() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let email_semantics = EmailSemantics {}; + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com", "email/send", None) + .unwrap(); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let attenuated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let chain = + ProofChain::try_from_token_string(attenuated_token.as_str(), None, &mut did_parser, &store) + .await + .unwrap(); + + let capability_infos = chain.reduce_capabilities(&email_semantics); + + assert_eq!(capability_infos.len(), 1); + + let info = capability_infos.get(0).unwrap(); + + assert_eq!( + info.capability.resource().to_string().as_str(), + "mailto:alice@email.com", + ); + assert_eq!(info.capability.ability().to_string().as_str(), "email/send"); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let email_semantics = EmailSemantics {}; + let send_email_as_bob = email_semantics + .parse("mailto:bob@email.com".into(), "email/send".into(), None) + .unwrap(); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .claiming_capability(&send_email_as_bob) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let capability_infos = + ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) + .await + .unwrap() + .reduce_capabilities(&email_semantics); + + assert_eq!(capability_infos.len(), 1); + + let info = capability_infos.get(0).unwrap(); + + assert_eq!( + info.originators.iter().collect::>(), + vec![&identities.bob_did] + ); + assert_eq!(info.capability, send_email_as_bob); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_finds_the_right_proof_chain_for_the_originator() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let email_semantics = EmailSemantics {}; + let send_email_as_bob = email_semantics + .parse("mailto:bob@email.com".into(), "email/send".into(), None) + .unwrap(); + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com".into(), "email/send".into(), None) + .unwrap(); + + let leaf_ucan_alice = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let leaf_ucan_bob = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capability(&send_email_as_bob) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.alice_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan_alice, None) + .witnessed_by(&leaf_ucan_bob, None) + .claiming_capability(&send_email_as_alice) + .claiming_capability(&send_email_as_bob) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan_token = ucan.encode().unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan_alice.encode().unwrap()) + .await + .unwrap(); + store + .write_token(&leaf_ucan_bob.encode().unwrap()) + .await + .unwrap(); + + let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) + .await + .unwrap(); + let capability_infos = proof_chain.reduce_capabilities(&email_semantics); + + assert_eq!(capability_infos.len(), 2); + + let send_email_as_bob_info = capability_infos.get(0).unwrap(); + let send_email_as_alice_info = capability_infos.get(1).unwrap(); + + assert_eq!( + send_email_as_alice_info, + &CapabilityInfo { + originators: BTreeSet::from_iter(vec![identities.alice_did]), + capability: send_email_as_alice, + not_before: ucan.not_before().clone(), + expires_at: ucan.expires_at().clone() + } + ); + + assert_eq!( + send_email_as_bob_info, + &CapabilityInfo { + originators: BTreeSet::from_iter(vec![identities.bob_did]), + capability: send_email_as_bob, + not_before: ucan.not_before().clone(), + expires_at: ucan.expires_at().clone() + } + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_reports_all_chain_options() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let email_semantics = EmailSemantics {}; + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com".into(), "email/send".into(), None) + .unwrap(); + + let leaf_ucan_alice = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let leaf_ucan_bob = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.alice_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan_alice, None) + .witnessed_by(&leaf_ucan_bob, None) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan_token = ucan.encode().unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan_alice.encode().unwrap()) + .await + .unwrap(); + store + .write_token(&leaf_ucan_bob.encode().unwrap()) + .await + .unwrap(); + + let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) + .await + .unwrap(); + let capability_infos = proof_chain.reduce_capabilities(&email_semantics); + + assert_eq!(capability_infos.len(), 1); + + let send_email_as_alice_info = capability_infos.get(0).unwrap(); + + assert_eq!( + send_email_as_alice_info, + &CapabilityInfo { + originators: BTreeSet::from_iter(vec![identities.alice_did, identities.bob_did]), + capability: send_email_as_alice, + not_before: ucan.not_before().clone(), + expires_at: ucan.expires_at().clone() + } + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_validates_caveats() -> anyhow::Result<()> { + let resource = "mailto:alice@email.com"; + let ability = "email/send"; + + let no_caveat = Capability::from((resource, ability, &json!({}))); + let x_caveat = Capability::from((resource, ability, &json!({ "x": true }))); + let y_caveat = Capability::from((resource, ability, &json!({ "y": true }))); + let z_caveat = Capability::from((resource, ability, &json!({ "z": true }))); + let yz_caveat = Capability::from((resource, ability, &json!({ "y": true, "z": true }))); + + let valid = [ + (vec![&no_caveat], vec![&no_caveat]), + (vec![&x_caveat], vec![&x_caveat]), + (vec![&no_caveat], vec![&x_caveat]), + (vec![&x_caveat, &y_caveat], vec![&x_caveat]), + (vec![&x_caveat, &y_caveat], vec![&x_caveat, &yz_caveat]), + ]; + + let invalid = [ + (vec![&x_caveat], vec![&no_caveat]), + (vec![&x_caveat], vec![&y_caveat]), + ( + vec![&x_caveat, &y_caveat], + vec![&x_caveat, &y_caveat, &z_caveat], + ), + ]; + + for (proof_capabilities, delegated_capabilities) in valid { + let is_successful = + test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; + assert!( + is_successful, + "{} enables {}", + render_caveats(&proof_capabilities), + render_caveats(&delegated_capabilities) + ); + } + + for (proof_capabilities, delegated_capabilities) in invalid { + let is_successful = + test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; + assert!( + !is_successful, + "{} disallows {}", + render_caveats(&proof_capabilities), + render_caveats(&delegated_capabilities) + ); + } + + fn render_caveats(capabilities: &Vec<&Capability>) -> String { + format!( + "{:?}", + capabilities + .iter() + .map(|cap| cap.caveat.to_string()) + .collect::>() + ) + } + + async fn test_capabilities_delegation( + proof_capabilities: &Vec<&Capability>, + delegated_capabilities: &Vec<&Capability>, + ) -> anyhow::Result { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let email_semantics = EmailSemantics {}; + let mut store = MemoryStore::default(); + let proof_capabilities = proof_capabilities + .to_owned() + .into_iter() + .map(|cap| cap.to_owned()) + .collect::>(); + let delegated_capabilities = delegated_capabilities + .to_owned() + .into_iter() + .map(|cap| cap.to_owned()) + .collect::>(); + + let proof_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(60) + .claiming_capabilities(&proof_capabilities) + .build()? + .sign() + .await?; + + let ucan = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.alice_did.as_str()) + .with_lifetime(50) + .witnessed_by(&proof_ucan, None) + .claiming_capabilities(&delegated_capabilities) + .build()? + .sign() + .await?; + store.write_token(&proof_ucan.encode().unwrap()).await?; + store.write_token(&ucan.encode().unwrap()).await?; + + let proof_chain = ProofChain::from_ucan(ucan, None, &mut did_parser, &store).await?; + + Ok(enables_capabilities( + &proof_chain, + &email_semantics, + &identities.alice_did, + &delegated_capabilities, + )) + } + + /// Checks proof chain returning true if all desired capabilities are enabled. + fn enables_capabilities( + proof_chain: &ProofChain, + semantics: &EmailSemantics, + originator: &String, + desired_capabilities: &Vec, + ) -> bool { + let capability_infos = proof_chain.reduce_capabilities(semantics); + + for desired_capability in desired_capabilities { + let mut has_capability = false; + for info in &capability_infos { + if info.originators.contains(originator) + && info + .capability + .enables(&semantics.parse_capability(desired_capability).unwrap()) + { + has_capability = true; + break; + } + } + if !has_capability { + return false; + } + } + true + } + + Ok(()) +} diff --git a/rust/noosphere-ucan/src/tests/builder.rs b/rust/noosphere-ucan/src/tests/builder.rs new file mode 100644 index 000000000..cb1fdeaf2 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/builder.rs @@ -0,0 +1,205 @@ +use std::collections::BTreeMap; + +use crate::{ + builder::UcanBuilder, + capability::{Capabilities, Capability, CapabilitySemantics}, + chain::ProofChain, + crypto::did::DidParser, + store::UcanJwtStore, + tests::fixtures::{ + Blake2bMemoryStore, EmailSemantics, Identities, WNFSSemantics, SUPPORTED_KEYS, + }, + time::now, +}; +use cid::multihash::Code; +use did_key::PatchedKeyPair; +use serde_json::json; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn it_builds_with_a_simple_example() { + let identities = Identities::new().await; + + let fact_1 = json!({ + "test": true + }); + + let fact_2 = json!({ + "preimage": "abc", + "hash": "sth" + }); + + let email_semantics = EmailSemantics {}; + let wnfs_semantics = WNFSSemantics {}; + + let cap_1 = email_semantics + .parse("mailto:alice@gmail.com", "email/send", None) + .unwrap(); + + let cap_2 = wnfs_semantics + .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) + .unwrap(); + + let expiration = now() + 30; + let not_before = now() - 30; + + let token = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_expiration(expiration) + .not_before(not_before) + .with_fact("abc/challenge", fact_1.clone()) + .with_fact("def/challenge", fact_2.clone()) + .claiming_capability(&cap_1) + .claiming_capability(&cap_2) + .with_nonce() + .build() + .unwrap(); + + let ucan = token.sign().await.unwrap(); + + assert_eq!(ucan.issuer(), identities.alice_did); + assert_eq!(ucan.audience(), identities.bob_did); + assert!(ucan.expires_at().is_some()); + assert_eq!(ucan.expires_at().unwrap(), expiration); + assert!(ucan.not_before().is_some()); + assert_eq!(ucan.not_before().unwrap(), not_before); + assert_eq!( + ucan.facts(), + &Some(BTreeMap::from([ + (String::from("abc/challenge"), fact_1), + (String::from("def/challenge"), fact_2), + ])) + ); + + let expected_attenuations = + Capabilities::try_from(vec![Capability::from(&cap_1), Capability::from(&cap_2)]).unwrap(); + + assert_eq!(ucan.capabilities(), &expected_attenuations); + assert!(ucan.nonce().is_some()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn it_builds_with_lifetime_in_seconds() { + let identities = Identities::new().await; + + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(300) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + assert!(ucan.expires_at().unwrap() > (now() + 290)); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn it_prevents_duplicate_proofs() { + let wnfs_semantics = WNFSSemantics {}; + + let parent_cap = wnfs_semantics + .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) + .unwrap(); + + let identities = Identities::new().await; + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(30) + .claiming_capability(&parent_cap) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let attenuated_cap_1 = wnfs_semantics + .parse("wnfs://alice.fission.name/public/Apps", "wnfs/create", None) + .unwrap(); + + let attenuated_cap_2 = wnfs_semantics + .parse( + "wnfs://alice.fission.name/public/Domains", + "wnfs/create", + None, + ) + .unwrap(); + + let next_ucan = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(30) + .witnessed_by(&ucan, None) + .claiming_capability(&attenuated_cap_1) + .claiming_capability(&attenuated_cap_2) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + assert_eq!( + next_ucan.proofs(), + &Some(vec![ucan + .to_cid(UcanBuilder::::default_hasher()) + .unwrap() + .to_string()]) + ) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_use_custom_hasher() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.alice_key) + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, Some(Code::Blake2b256)) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let mut store = Blake2bMemoryStore::default(); + + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let _ = store + .write_token(&delegated_token.encode().unwrap()) + .await + .unwrap(); + + let valid_chain = + ProofChain::from_ucan(delegated_token, Some(now()), &mut did_parser, &store).await; + + assert!(valid_chain.is_ok()); +} diff --git a/rust/noosphere-ucan/src/tests/capability.rs b/rust/noosphere-ucan/src/tests/capability.rs new file mode 100644 index 000000000..11e9bd2d8 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/capability.rs @@ -0,0 +1,91 @@ +use crate::capability::{Capabilities, Capability}; +use serde_json::json; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn it_can_cast_between_map_and_sequence() { + let cap_foo = Capability::from(("example://foo", "ability/foo", &json!({}))); + let cap_bar_1 = Capability::from(("example://bar", "ability/bar", &json!({ "beep": 1 }))); + let cap_bar_2 = Capability::from(("example://bar", "ability/bar", &json!({ "boop": 1 }))); + + let cap_sequence = vec![cap_bar_1.clone(), cap_bar_2.clone(), cap_foo]; + let cap_map = Capabilities::try_from(&json!({ + "example://bar": { + "ability/bar": [{ "beep": 1 }, { "boop": 1 }] + }, + "example://foo": { "ability/foo": [{}] }, + })) + .unwrap(); + + assert_eq!( + &cap_map.iter().collect::>(), + &cap_sequence, + "Capabilities map to sequence." + ); + assert_eq!( + &Capabilities::try_from(cap_sequence).unwrap(), + &cap_map, + "Capabilities sequence to map." + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn it_rejects_non_compliant_json() { + let failure_cases = [ + (json!([]), "resources must be map"), + ( + json!({ + "resource:foo": [] + }), + "abilities must be map", + ), + ( + json!({"resource:foo": {}}), + "resource must have at least one ability", + ), + ( + json!({"resource:foo": { "ability/read": {} }}), + "caveats must be array", + ), + ( + json!({"resource:foo": { "ability/read": [1] }}), + "caveat must be object", + ), + ]; + + for (json_data, message) in failure_cases { + assert!(Capabilities::try_from(&json_data).is_err(), "{message}"); + } + + assert!(Capabilities::try_from(&json!({ + "resource:foo": { "ability/read": [{}] } + })) + .is_ok()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn it_filters_out_empty_caveats_when_iterating() { + let cap_map = Capabilities::try_from(&json!({ + "example://bar": { "ability/bar": [{}] }, + "example://foo": { "ability/foo": [] } + })) + .unwrap(); + + assert_eq!( + cap_map.iter().collect::>(), + vec![Capability::from(( + "example://bar", + "ability/bar", + &json!({}) + ))], + "iter() filters out capabilities with empty caveats" + ); +} diff --git a/rust/noosphere-ucan/src/tests/chain.rs b/rust/noosphere-ucan/src/tests/chain.rs new file mode 100644 index 000000000..d671b590f --- /dev/null +++ b/rust/noosphere-ucan/src/tests/chain.rs @@ -0,0 +1,254 @@ +use super::fixtures::{Identities, SUPPORTED_KEYS}; +use crate::{ + builder::UcanBuilder, + chain::ProofChain, + crypto::did::DidParser, + store::{MemoryStore, UcanJwtStore}, + time::now, +}; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_decodes_deep_ucan_chains() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let chain = + ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) + .await + .unwrap(); + + assert_eq!(chain.ucan().audience(), &identities.mallory_did); + assert_eq!( + chain.proofs().get(0).unwrap().ucan().issuer(), + &identities.alice_did + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_fails_with_incorrect_chaining() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let parse_token_result = + ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) + .await; + + assert!(parse_token_result.is_err()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_be_instantiated_by_cid() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let cid = store.write_token(&delegated_token).await.unwrap(); + + let chain = ProofChain::from_cid(&cid, None, &mut did_parser, &store) + .await + .unwrap(); + + assert_eq!(chain.ucan().audience(), &identities.mallory_did); + assert_eq!( + chain.proofs().get(0).unwrap().ucan().issuer(), + &identities.alice_did + ); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_handle_multiple_leaves() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan_1 = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let leaf_ucan_2 = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.alice_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan_1, None) + .witnessed_by(&leaf_ucan_2, None) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + store + .write_token(&leaf_ucan_1.encode().unwrap()) + .await + .unwrap(); + store + .write_token(&leaf_ucan_2.encode().unwrap()) + .await + .unwrap(); + + ProofChain::try_from_token_string(&delegated_token, None, &mut did_parser, &store) + .await + .unwrap(); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +pub async fn it_can_use_a_custom_timestamp_to_validate_a_ucan() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let leaf_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(60) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let delegated_token = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_lifetime(50) + .witnessed_by(&leaf_ucan, None) + .build() + .unwrap() + .sign() + .await + .unwrap() + .encode() + .unwrap(); + + let mut store = MemoryStore::default(); + + store + .write_token(&leaf_ucan.encode().unwrap()) + .await + .unwrap(); + + let cid = store.write_token(&delegated_token).await.unwrap(); + + let valid_chain = ProofChain::from_cid(&cid, Some(now()), &mut did_parser, &store).await; + + assert!(valid_chain.is_ok()); + + let invalid_chain = ProofChain::from_cid(&cid, Some(now() + 61), &mut did_parser, &store).await; + + assert!(invalid_chain.is_err()); +} diff --git a/rust/noosphere-ucan/src/tests/crypto.rs b/rust/noosphere-ucan/src/tests/crypto.rs new file mode 100644 index 000000000..0b40b155f --- /dev/null +++ b/rust/noosphere-ucan/src/tests/crypto.rs @@ -0,0 +1,27 @@ +mod did_from_keypair { + use base64::Engine; + use did_key::{from_existing_key, Ed25519KeyPair, KeyMaterial as _KeyMaterial}; + + use crate::crypto::KeyMaterial; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_handles_ed25519_keys() { + let pub_key = base64::engine::general_purpose::STANDARD + .decode("Hv+AVRD2WUjUFOsSNbsmrp9fokuwrUnjBcr92f0kxw4=") + .unwrap(); + let key = Ed25519KeyPair::from_public_key(&pub_key); + let keypair = from_existing_key::(&key.public_key_bytes(), None); + + let expected_did = "did:key:z6MkgYGF3thn8k1Fv4p4dWXKtsXCnLH7q9yw4QgNPULDmDKB"; + let result_did = keypair.get_did().await.unwrap(); + + assert_eq!(expected_did, result_did.as_str()); + } +} diff --git a/rust/noosphere-ucan/src/tests/fixtures/capabilities/email.rs b/rust/noosphere-ucan/src/tests/fixtures/capabilities/email.rs new file mode 100644 index 000000000..d8e445249 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/capabilities/email.rs @@ -0,0 +1,63 @@ +use crate::capability::{Ability, CapabilitySemantics, Scope}; +use anyhow::{anyhow, Result}; +use url::Url; + +#[derive(Clone, PartialEq)] +pub struct EmailAddress(String); + +impl Scope for EmailAddress { + fn contains(&self, other: &Self) -> bool { + return self.0 == other.0; + } +} + +impl ToString for EmailAddress { + fn to_string(&self) -> String { + format!("mailto:{}", self.0.clone()) + } +} + +impl TryFrom for EmailAddress { + type Error = anyhow::Error; + + fn try_from(value: Url) -> Result { + match value.scheme() { + "mailto" => Ok(EmailAddress(String::from(value.path()))), + _ => Err(anyhow!( + "Could not interpret URI as an email address: {}", + value + )), + } + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum EmailAction { + Send, +} + +impl Ability for EmailAction {} + +impl ToString for EmailAction { + fn to_string(&self) -> String { + match self { + EmailAction::Send => "email/send", + } + .into() + } +} + +impl TryFrom for EmailAction { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + match value.as_str() { + "email/send" => Ok(EmailAction::Send), + _ => Err(anyhow!("Unrecognized action: {}", value)), + } + } +} + +pub struct EmailSemantics {} + +impl CapabilitySemantics for EmailSemantics {} diff --git a/rust/noosphere-ucan/src/tests/fixtures/capabilities/mod.rs b/rust/noosphere-ucan/src/tests/fixtures/capabilities/mod.rs new file mode 100644 index 000000000..ee70eec21 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/capabilities/mod.rs @@ -0,0 +1,5 @@ +mod email; +mod wnfs; + +pub use email::*; +pub use wnfs::*; diff --git a/rust/noosphere-ucan/src/tests/fixtures/capabilities/wnfs.rs b/rust/noosphere-ucan/src/tests/fixtures/capabilities/wnfs.rs new file mode 100644 index 000000000..4b18a3c6c --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/capabilities/wnfs.rs @@ -0,0 +1,96 @@ +use crate::capability::{Ability, CapabilitySemantics, Scope}; +use anyhow::{anyhow, Result}; +use url::Url; + +#[derive(Ord, Eq, PartialOrd, PartialEq, Clone)] +pub enum WNFSCapLevel { + Create, + Revise, + SoftDelete, + Overwrite, + SuperUser, +} + +impl Ability for WNFSCapLevel {} + +impl TryFrom for WNFSCapLevel { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + Ok(match value.as_str() { + "wnfs/create" => WNFSCapLevel::Create, + "wnfs/revise" => WNFSCapLevel::Revise, + "wnfs/soft_delete" => WNFSCapLevel::SoftDelete, + "wnfs/overwrite" => WNFSCapLevel::Overwrite, + "wnfs/super_user" => WNFSCapLevel::SuperUser, + _ => return Err(anyhow!("No such WNFS capability level: {}", value)), + }) + } +} + +impl ToString for WNFSCapLevel { + fn to_string(&self) -> String { + match self { + WNFSCapLevel::Create => "wnfs/create", + WNFSCapLevel::Revise => "wnfs/revise", + WNFSCapLevel::SoftDelete => "wnfs/soft_delete", + WNFSCapLevel::Overwrite => "wnfs/overwrite", + WNFSCapLevel::SuperUser => "wnfs/super_user", + } + .into() + } +} + +#[derive(Clone, PartialEq)] +pub struct WNFSScope { + origin: String, + path: String, +} + +impl Scope for WNFSScope { + fn contains(&self, other: &Self) -> bool { + if self.origin != other.origin { + return false; + } + + let self_path_parts = self.path.split('/'); + let mut other_path_parts = other.path.split('/'); + + for part in self_path_parts { + match other_path_parts.nth(0) { + Some(other_part) => { + if part != other_part { + return false; + } + } + None => return false, + } + } + + true + } +} + +impl TryFrom for WNFSScope { + type Error = anyhow::Error; + + fn try_from(value: Url) -> Result { + match (value.scheme(), value.host_str(), value.path()) { + ("wnfs", Some(host), path) => Ok(WNFSScope { + origin: String::from(host), + path: String::from(path), + }), + _ => Err(anyhow!("Cannot interpret URI as WNFS scope: {}", value)), + } + } +} + +impl ToString for WNFSScope { + fn to_string(&self) -> String { + format!("wnfs://{}{}", self.origin, self.path) + } +} + +pub struct WNFSSemantics {} + +impl CapabilitySemantics for WNFSSemantics {} diff --git a/rust/noosphere-ucan/src/tests/fixtures/crypto.rs b/rust/noosphere-ucan/src/tests/fixtures/crypto.rs new file mode 100644 index 000000000..c2fcfc981 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/crypto.rs @@ -0,0 +1,39 @@ +use crate::crypto::{ + did::{KeyConstructorSlice, ED25519_MAGIC_BYTES}, + KeyMaterial, +}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use did_key::{from_existing_key, CoreSign, Ed25519KeyPair, Fingerprint, PatchedKeyPair}; + +pub const SUPPORTED_KEYS: &KeyConstructorSlice = &[ + // https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L94 + (ED25519_MAGIC_BYTES, bytes_to_ed25519_key), +]; + +pub fn bytes_to_ed25519_key(bytes: Vec) -> Result> { + Ok(Box::new(from_existing_key::( + bytes.as_slice(), + None, + ))) +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl KeyMaterial for PatchedKeyPair { + fn get_jwt_algorithm_name(&self) -> String { + "EdDSA".into() + } + + async fn get_did(&self) -> Result { + Ok(format!("did:key:{}", self.fingerprint())) + } + + async fn sign(&self, payload: &[u8]) -> Result> { + Ok(CoreSign::sign(self, payload)) + } + + async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + CoreSign::verify(self, payload, signature).map_err(|error| anyhow!("{:?}", error)) + } +} diff --git a/rust/noosphere-ucan/src/tests/fixtures/identities.rs b/rust/noosphere-ucan/src/tests/fixtures/identities.rs new file mode 100644 index 000000000..df199d6a3 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/identities.rs @@ -0,0 +1,60 @@ +use base64::Engine; +use did_key::{ + from_existing_key, Ed25519KeyPair, Generate, KeyMaterial as _KeyMaterial, PatchedKeyPair, +}; + +use crate::crypto::KeyMaterial; + +pub struct Identities { + pub alice_key: PatchedKeyPair, + pub bob_key: PatchedKeyPair, + pub mallory_key: PatchedKeyPair, + + pub alice_did: String, + pub bob_did: String, + pub mallory_did: String, +} + +/// An adaptation of the fixtures used in the canonical ts-ucan repo +/// See: https://github.com/ucan-wg/ts-ucan/blob/main/tests/fixtures.ts +impl Identities { + pub async fn new() -> Self { + // NOTE: tweetnacl secret keys concat the public keys, so we only care + // about the first 32 bytes + let alice_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==".as_bytes()).unwrap().as_slice()[0..32]); + let alice_keypair = from_existing_key::( + &alice_key.public_key_bytes(), + Some(&alice_key.private_key_bytes()), + ); + let bob_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("G4+QCX1b3a45IzQsQd4gFMMe0UB1UOx9bCsh8uOiKLER69eAvVXvc8P2yc4Iig42Bv7JD2zJxhyFALyTKBHipg==".as_bytes()).unwrap().as_slice()[0..32]); + let bob_keypair = from_existing_key::( + &bob_key.public_key_bytes(), + Some(&bob_key.private_key_bytes()), + ); + let mallory_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("LR9AL2MYkMARuvmV3MJV8sKvbSOdBtpggFCW8K62oZDR6UViSXdSV/dDcD8S9xVjS61vh62JITx7qmLgfQUSZQ==".as_bytes()).unwrap().as_slice()[0..32]); + let mallory_keypair = from_existing_key::( + &mallory_key.public_key_bytes(), + Some(&mallory_key.private_key_bytes()), + ); + + Identities { + alice_did: alice_keypair.get_did().await.unwrap(), + bob_did: bob_keypair.get_did().await.unwrap(), + mallory_did: mallory_keypair.get_did().await.unwrap(), + + alice_key: alice_keypair, + bob_key: bob_keypair, + mallory_key: mallory_keypair, + } + } + + #[allow(dead_code)] + pub fn name_for(&self, did: String) -> String { + match did { + _ if did == self.alice_did => "alice".into(), + _ if did == self.bob_did => "bob".into(), + _ if did == self.mallory_did => "mallory".into(), + _ => did, + } + } +} diff --git a/rust/noosphere-ucan/src/tests/fixtures/mod.rs b/rust/noosphere-ucan/src/tests/fixtures/mod.rs new file mode 100644 index 000000000..82669dbda --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/mod.rs @@ -0,0 +1,9 @@ +mod capabilities; +mod crypto; +mod identities; +mod store; + +pub use capabilities::*; +pub use crypto::*; +pub use identities::*; +pub use store::*; diff --git a/rust/noosphere-ucan/src/tests/fixtures/store.rs b/rust/noosphere-ucan/src/tests/fixtures/store.rs new file mode 100644 index 000000000..bd158760e --- /dev/null +++ b/rust/noosphere-ucan/src/tests/fixtures/store.rs @@ -0,0 +1,48 @@ +use crate::store::{UcanStore, UcanStoreConditionalSend}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use cid::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use libipld_core::{ + codec::{Codec, Decode, Encode}, + raw::RawCodec, +}; +use std::{ + collections::HashMap, + io::Cursor, + sync::{Arc, Mutex}, +}; + +#[derive(Clone, Default, Debug)] +pub struct Blake2bMemoryStore { + dags: Arc>>>, +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl UcanStore for Blake2bMemoryStore { + async fn read>(&self, cid: &Cid) -> Result> { + let dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + + Ok(match dags.get(cid) { + Some(bytes) => Some(T::decode(RawCodec, &mut Cursor::new(bytes))?), + None => None, + }) + } + + async fn write + UcanStoreConditionalSend + core::fmt::Debug>( + &mut self, + token: T, + ) -> Result { + let codec = RawCodec; + let block = codec.encode(&token)?; + let cid = Cid::new_v1(codec.into(), Code::Blake2b256.digest(&block)); + + let mut dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; + dags.insert(cid, block); + + Ok(cid) + } +} diff --git a/rust/noosphere-ucan/src/tests/helpers.rs b/rust/noosphere-ucan/src/tests/helpers.rs new file mode 100644 index 000000000..c178a4c07 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/helpers.rs @@ -0,0 +1,56 @@ +use super::fixtures::{EmailSemantics, Identities}; +use crate::{builder::UcanBuilder, capability::CapabilitySemantics}; +use anyhow::Result; +use did_key::PatchedKeyPair; +use serde::{de::DeserializeOwned, Serialize}; +use serde_ipld_dagcbor::{from_slice, to_vec}; + +pub fn dag_cbor_roundtrip(data: &T) -> Result +where + T: Serialize + DeserializeOwned, +{ + Ok(from_slice(&to_vec(data)?)?) +} + +pub async fn scaffold_ucan_builder(identities: &Identities) -> Result> { + let email_semantics = EmailSemantics {}; + let send_email_as_bob = email_semantics + .parse("mailto:bob@email.com".into(), "email/send".into(), None) + .unwrap(); + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com".into(), "email/send".into(), None) + .unwrap(); + + let leaf_ucan_alice = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.mallory_did.as_str()) + .with_expiration(1664232146010) + .claiming_capability(&send_email_as_alice) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let leaf_ucan_bob = UcanBuilder::default() + .issued_by(&identities.bob_key) + .for_audience(identities.mallory_did.as_str()) + .with_expiration(1664232146010) + .claiming_capability(&send_email_as_bob) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let builder = UcanBuilder::default() + .issued_by(&identities.mallory_key) + .for_audience(identities.alice_did.as_str()) + .with_expiration(1664232146010) + .witnessed_by(&leaf_ucan_alice, None) + .witnessed_by(&leaf_ucan_bob, None) + .claiming_capability(&send_email_as_alice) + .claiming_capability(&send_email_as_bob); + + Ok(builder) +} diff --git a/rust/noosphere-ucan/src/tests/mod.rs b/rust/noosphere-ucan/src/tests/mod.rs new file mode 100644 index 000000000..1a8c23559 --- /dev/null +++ b/rust/noosphere-ucan/src/tests/mod.rs @@ -0,0 +1,8 @@ +mod attenuation; +mod builder; +mod capability; +mod chain; +mod crypto; +pub mod fixtures; +pub mod helpers; +mod ucan; diff --git a/rust/noosphere-ucan/src/tests/ucan.rs b/rust/noosphere-ucan/src/tests/ucan.rs new file mode 100644 index 000000000..5f2b4b12b --- /dev/null +++ b/rust/noosphere-ucan/src/tests/ucan.rs @@ -0,0 +1,235 @@ +mod validate { + use crate::{ + builder::UcanBuilder, + capability::CapabilitySemantics, + crypto::did::DidParser, + tests::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}, + time::now, + ucan::Ucan, + }; + use anyhow::Result; + + use serde_json::json; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test_configure!(run_in_browser); + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_round_trips_with_encode() { + let identities = Identities::new().await; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(30) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let encoded_ucan = ucan.encode().unwrap(); + let decoded_ucan = Ucan::try_from(encoded_ucan.as_str()).unwrap(); + + decoded_ucan.validate(None, &mut did_parser).await.unwrap(); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_identifies_a_ucan_that_is_not_active_yet() { + let identities = Identities::new().await; + + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .not_before(now() + 30) + .with_lifetime(30) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + assert!(ucan.is_too_early()); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_identifies_a_ucan_that_has_become_active() { + let identities = Identities::new().await; + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .not_before(now() / 1000) + .with_lifetime(30) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + assert!(!ucan.is_too_early()); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_be_serialized_as_json() -> Result<()> { + let identities = Identities::new().await; + + let email_semantics = EmailSemantics {}; + let send_email_as_alice = email_semantics + .parse("mailto:alice@email.com".into(), "email/send".into(), None) + .unwrap(); + + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .not_before(now() / 1000) + .with_lifetime(30) + .with_fact("abc/challenge", json!({ "foo": "bar" })) + .claiming_capability(&send_email_as_alice) + .build()? + .sign() + .await?; + + let ucan_json = serde_json::to_value(ucan.clone())?; + + assert_eq!( + ucan_json, + serde_json::json!({ + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": crate::ucan::UCAN_VERSION, + "iss": ucan.issuer(), + "aud": ucan.audience(), + "exp": ucan.expires_at(), + "nbf": ucan.not_before(), + "cap": { + "mailto:alice@email.com": { + "email/send": [{}] + } + }, + "fct": { + "abc/challenge": { "foo": "bar" } + } + }, + "signed_data": ucan.signed_data(), + "signature": ucan.signature() + }) + ); + Ok(()) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_can_be_serialized_as_json_without_optionals() -> Result<()> { + let identities = Identities::new().await; + let ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .build()? + .sign() + .await?; + + let ucan_json = serde_json::to_value(ucan.clone())?; + + assert_eq!( + ucan_json, + serde_json::json!({ + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": crate::ucan::UCAN_VERSION, + "iss": ucan.issuer(), + "aud": ucan.audience(), + "exp": serde_json::Value::Null, + "cap": {} + }, + "signed_data": ucan.signed_data(), + "signature": ucan.signature() + }) + ); + + Ok(()) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_implements_partial_eq() { + let identities = Identities::new().await; + let ucan_a = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_expiration(10000000) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan_b = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_expiration(10000000) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + let ucan_c = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_expiration(20000000) + .build() + .unwrap() + .sign() + .await + .unwrap(); + + assert!(ucan_a == ucan_b); + assert!(ucan_a != ucan_c); + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn test_lifetime_ends_after() -> Result<()> { + let identities = Identities::new().await; + let forever_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .build()? + .sign() + .await?; + let early_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(2000) + .build()? + .sign() + .await?; + let later_ucan = UcanBuilder::default() + .issued_by(&identities.alice_key) + .for_audience(identities.bob_did.as_str()) + .with_lifetime(4000) + .build()? + .sign() + .await?; + + assert_eq!(*forever_ucan.expires_at(), None); + assert!(forever_ucan.lifetime_ends_after(&early_ucan)); + assert!(!early_ucan.lifetime_ends_after(&forever_ucan)); + assert!(later_ucan.lifetime_ends_after(&early_ucan)); + + Ok(()) + } +} diff --git a/rust/noosphere-ucan/src/time.rs b/rust/noosphere-ucan/src/time.rs new file mode 100644 index 000000000..af8b5b0bc --- /dev/null +++ b/rust/noosphere-ucan/src/time.rs @@ -0,0 +1,8 @@ +use instant::SystemTime; + +pub fn now() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/rust/noosphere-ucan/src/ucan.rs b/rust/noosphere-ucan/src/ucan.rs new file mode 100644 index 000000000..811f8552b --- /dev/null +++ b/rust/noosphere-ucan/src/ucan.rs @@ -0,0 +1,270 @@ +use crate::{ + capability::Capabilities, + crypto::did::DidParser, + serde::{Base64Encode, DagJson}, + time::now, +}; +use anyhow::{anyhow, Result}; +use base64::Engine; +use cid::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use libipld_core::{codec::Codec, raw::RawCodec}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{collections::BTreeMap, str::FromStr}; + +pub const UCAN_VERSION: &str = "0.10.0-canary"; + +pub type FactsMap = BTreeMap; + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct UcanHeader { + pub alg: String, + pub typ: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct UcanPayload { + pub ucv: String, + pub iss: String, + pub aud: String, + pub exp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nbf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nnc: Option, + pub cap: Capabilities, + #[serde(skip_serializing_if = "Option::is_none")] + pub fct: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Ucan { + header: UcanHeader, + payload: UcanPayload, + signed_data: Vec, + signature: Vec, +} + +impl Ucan { + pub fn new( + header: UcanHeader, + payload: UcanPayload, + signed_data: Vec, + signature: Vec, + ) -> Self { + Ucan { + signed_data, + header, + payload, + signature, + } + } + + /// Validate the UCAN's signature and timestamps + pub async fn validate<'a>( + &self, + now_time: Option, + did_parser: &mut DidParser, + ) -> Result<()> { + if self.is_expired(now_time) { + return Err(anyhow!("Expired")); + } + + if self.is_too_early() { + return Err(anyhow!("Not active yet (too early)")); + } + + self.check_signature(did_parser).await + } + + /// Validate that the signed data was signed by the stated issuer + pub async fn check_signature<'a>(&self, did_parser: &mut DidParser) -> Result<()> { + let key = did_parser.parse(&self.payload.iss)?; + key.verify(&self.signed_data, &self.signature).await + } + + /// Produce a base64-encoded serialization of the UCAN suitable for + /// transferring in a header field + pub fn encode(&self) -> Result { + let header = self.header.jwt_base64_encode()?; + let payload = self.payload.jwt_base64_encode()?; + let signature = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.signature.as_slice()); + + Ok(format!("{header}.{payload}.{signature}")) + } + + /// Returns true if the UCAN has past its expiration date + pub fn is_expired(&self, now_time: Option) -> bool { + if let Some(exp) = self.payload.exp { + exp < now_time.unwrap_or_else(now) + } else { + false + } + } + + /// Raw bytes of signed data for this UCAN + pub fn signed_data(&self) -> &[u8] { + &self.signed_data + } + + pub fn signature(&self) -> &[u8] { + &self.signature + } + + /// Returns true if the not-before ("nbf") time is still in the future + pub fn is_too_early(&self) -> bool { + match self.payload.nbf { + Some(nbf) => nbf > now(), + None => false, + } + } + + /// Returns true if this UCAN's lifetime begins no later than the other + /// Note that if a UCAN specifies an NBF but the other does not, the + /// other has an unbounded start time and this function will return + /// false. + pub fn lifetime_begins_before(&self, other: &Ucan) -> bool { + match (self.payload.nbf, other.payload.nbf) { + (Some(nbf), Some(other_nbf)) => nbf <= other_nbf, + (Some(_), None) => false, + _ => true, + } + } + + /// Returns true if this UCAN expires no earlier than the other + pub fn lifetime_ends_after(&self, other: &Ucan) -> bool { + match (self.payload.exp, other.payload.exp) { + (Some(exp), Some(other_exp)) => exp >= other_exp, + (Some(_), None) => false, + (None, _) => true, + } + } + + /// Returns true if this UCAN's lifetime fully encompasses the other + pub fn lifetime_encompasses(&self, other: &Ucan) -> bool { + self.lifetime_begins_before(other) && self.lifetime_ends_after(other) + } + + pub fn algorithm(&self) -> &str { + &self.header.alg + } + + pub fn issuer(&self) -> &str { + &self.payload.iss + } + + pub fn audience(&self) -> &str { + &self.payload.aud + } + + pub fn proofs(&self) -> &Option> { + &self.payload.prf + } + + pub fn expires_at(&self) -> &Option { + &self.payload.exp + } + + pub fn not_before(&self) -> &Option { + &self.payload.nbf + } + + pub fn nonce(&self) -> &Option { + &self.payload.nnc + } + + #[deprecated(since = "0.4.0", note = "use `capabilities()`")] + pub fn attenuation(&self) -> &Capabilities { + self.capabilities() + } + + pub fn capabilities(&self) -> &Capabilities { + &self.payload.cap + } + + pub fn facts(&self) -> &Option { + &self.payload.fct + } + + pub fn version(&self) -> &str { + &self.payload.ucv + } + + pub fn to_cid(&self, hasher: Code) -> Result { + let codec = RawCodec; + let token = self.encode()?; + let encoded = codec.encode(token.as_bytes())?; + Ok(Cid::new_v1(codec.into(), hasher.digest(&encoded))) + } +} + +/// Deserialize an encoded UCAN token string reference into a UCAN +impl<'a> TryFrom<&'a str> for Ucan { + type Error = anyhow::Error; + + fn try_from(ucan_token: &str) -> Result { + Ucan::from_str(ucan_token) + } +} + +/// Deserialize an encoded UCAN token string into a UCAN +impl TryFrom for Ucan { + type Error = anyhow::Error; + + fn try_from(ucan_token: String) -> Result { + Ucan::from_str(ucan_token.as_str()) + } +} + +/// Deserialize an encoded UCAN token string reference into a UCAN +impl FromStr for Ucan { + type Err = anyhow::Error; + + fn from_str(ucan_token: &str) -> Result { + // better to create multiple iterators than collect, or clone. + let signed_data = ucan_token + .split('.') + .take(2) + .map(String::from) + .reduce(|l, r| format!("{l}.{r}")) + .ok_or_else(|| anyhow!("Could not parse signed data from token string"))?; + + let mut parts = ucan_token.split('.').map(|str| { + base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(str) + .map_err(|error| anyhow!(error)) + }); + + let header = parts + .next() + .ok_or_else(|| anyhow!("Missing UCAN header in token part"))? + .map(|decoded| UcanHeader::from_dag_json(&decoded)) + .map_err(|e| e.context("Could not decode UCAN header base64"))? + .map_err(|e| e.context("Could not parse UCAN header JSON"))?; + + let payload = parts + .next() + .ok_or_else(|| anyhow!("Missing UCAN payload in token part"))? + .map(|decoded| UcanPayload::from_dag_json(&decoded)) + .map_err(|e| e.context("Could not decode UCAN payload base64"))? + .map_err(|e| e.context("Could not parse UCAN payload JSON"))?; + + let signature = parts + .next() + .ok_or_else(|| anyhow!("Missing UCAN signature in token part"))? + .map_err(|e| e.context("Could not parse UCAN signature base64"))?; + + Ok(Ucan::new( + header, + payload, + signed_data.as_bytes().into(), + signature, + )) + } +} diff --git a/rust/noosphere/Cargo.toml b/rust/noosphere/Cargo.toml index b53410a90..45d0651a0 100644 --- a/rust/noosphere/Cargo.toml +++ b/rust/noosphere/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" description = "A high-level package for dealing with accessing the Noosphere" keywords = ["noosphere"] categories = ["filesystem"] -rust-version = "1.70.0" +rust-version = "1.75.0" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/noosphere" repository = "https://github.com/subconsciousnetwork/noosphere" @@ -28,7 +28,7 @@ pkg-version = "^1.0.0" thiserror = { workspace = true } lazy_static = "^1" cid = { workspace = true } -async-trait = "~0.1" +async-trait = { workspace = true } async-stream = { workspace = true } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } @@ -42,19 +42,19 @@ libipld-core = { workspace = true } libipld-cbor = { workspace = true } bytes = "^1" -noosphere-core = { version = "0.18.1", path = "../noosphere-core" } -noosphere-storage = { version = "0.10.1", path = "../noosphere-storage" } -noosphere-ipfs = { version = "0.8.6", path = "../noosphere-ipfs", optional = true } -ucan = { workspace = true } -ucan-key-support = { workspace = true } +noosphere-core = { workspace = true } +noosphere-storage = { workspace = true } +noosphere-ipfs = { workspace = true, optional = true } +noosphere-ucan = { workspace = true } +noosphere-ucan-key-support = { workspace = true } [dev-dependencies] libipld-core = { workspace = true } rand = { workspace = true } serde_json = { workspace = true } -noosphere-core = { version = "0.18.1", path = "../noosphere-core", features = ["helpers"] } -noosphere-common = { version = "0.1.2", path = "../noosphere-common", features = ["helpers"] } +noosphere-core = { workspace = true, features = ["helpers"] } +noosphere-common = { workspace = true, features = ["helpers"] } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: We should eventually support gateway storage as a specialty target only, @@ -64,7 +64,7 @@ rexie = { version = "~0.5" } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } js-sys = { workspace = true } -noosphere-into = { version = "0.11.6", path = "../noosphere-into" } +noosphere-into = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] workspace = true diff --git a/rust/noosphere/src/key/insecure.rs b/rust/noosphere/src/key/insecure.rs index bc9dbc9a5..6f9250200 100644 --- a/rust/noosphere/src/key/insecure.rs +++ b/rust/noosphere/src/key/insecure.rs @@ -6,13 +6,13 @@ use noosphere_core::{ authority::{ed25519_key_to_mnemonic, generate_ed25519_key, restore_ed25519_key}, data::Did, }; +use noosphere_ucan::crypto::KeyMaterial; +use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; use std::{ collections::BTreeMap, path::{Path, PathBuf}, }; use tokio::fs; -use ucan::crypto::KeyMaterial; -use ucan_key_support::ed25519::Ed25519KeyMaterial; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; @@ -148,9 +148,9 @@ impl KeyStorage for InsecureKeyStorage { mod tests { use super::*; use crate::key::KeyStorage; + use noosphere_ucan::crypto::KeyMaterial; use tempfile::TempDir; use tokio::fs; - use ucan::crypto::KeyMaterial; #[tokio::test] async fn it_can_create_and_read_a_key() { diff --git a/rust/noosphere/src/key/interface.rs b/rust/noosphere/src/key/interface.rs index 7010e4aa6..3afee38d1 100644 --- a/rust/noosphere/src/key/interface.rs +++ b/rust/noosphere/src/key/interface.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use ucan::crypto::KeyMaterial; +use noosphere_ucan::crypto::KeyMaterial; /// A trait that represents access to arbitrary key storage backends. #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/rust/noosphere/src/key/web.rs b/rust/noosphere/src/key/web.rs index 3fc0c7621..3f9744cca 100644 --- a/rust/noosphere/src/key/web.rs +++ b/rust/noosphere/src/key/web.rs @@ -2,9 +2,9 @@ use std::rc::Rc; use anyhow::{anyhow, Result}; use async_trait::async_trait; +use noosphere_ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; use rexie::{KeyRange, ObjectStore, Rexie, RexieBuilder, Store, Transaction, TransactionMode}; use std::sync::Arc; -use ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; use wasm_bindgen::{JsCast, JsValue}; use web_sys::CryptoKey; @@ -152,7 +152,7 @@ mod tests { use crate::key::KeyStorage; use super::WebCryptoKeyStorage; - use ucan::crypto::KeyMaterial; + use noosphere_ucan::crypto::KeyMaterial; use wasm_bindgen_test::wasm_bindgen_test; diff --git a/rust/noosphere/src/platform.rs b/rust/noosphere/src/platform.rs index af409887a..0afc11c38 100644 --- a/rust/noosphere/src/platform.rs +++ b/rust/noosphere/src/platform.rs @@ -7,7 +7,7 @@ mod inner { #![allow(missing_docs)] - use ucan_key_support::ed25519::Ed25519KeyMaterial; + use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; use crate::key::InsecureKeyStorage; @@ -55,8 +55,8 @@ mod inner { use crate::key::WebCryptoKeyStorage; + use noosphere_ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; use std::sync::Arc; - use ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; pub type PlatformKeyMaterial = Arc; pub type PlatformKeyStorage = WebCryptoKeyStorage; @@ -103,7 +103,7 @@ mod inner { #[cfg(all(native, not(apple)))] mod inner { use crate::key::InsecureKeyStorage; - use ucan_key_support::ed25519::Ed25519KeyMaterial; + use noosphere_ucan_key_support::ed25519::Ed25519KeyMaterial; /// The default key type produced by the [crate::key::KeyStorage] /// implementation in use for this platform diff --git a/rust/noosphere/src/sphere/builder/recover.rs b/rust/noosphere/src/sphere/builder/recover.rs index 1bff3e44c..4a88264f3 100644 --- a/rust/noosphere/src/sphere/builder/recover.rs +++ b/rust/noosphere/src/sphere/builder/recover.rs @@ -13,8 +13,8 @@ use noosphere_core::{ stream::put_block_stream, }; use noosphere_storage::KeyValueStore; +use noosphere_ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use tokio::sync::Mutex; -use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; use crate::{ key::KeyStorage, diff --git a/rust/noosphere/tests/client_to_gateway.rs b/rust/noosphere/tests/client_to_gateway.rs index d8e425a88..84f63f88a 100644 --- a/rust/noosphere/tests/client_to_gateway.rs +++ b/rust/noosphere/tests/client_to_gateway.rs @@ -28,10 +28,10 @@ use noosphere_core::data::{ContentType, Did}; use noosphere_core::tracing::initialize_tracing; use noosphere_gateway::{Gateway, SingleTenantGatewayManager}; use noosphere_storage::BlockStore; +use noosphere_ucan::crypto::KeyMaterial; use std::net::TcpListener; use tokio::io::AsyncReadExt; use tokio_stream::StreamExt; -use ucan::crypto::KeyMaterial; use url::Url; #[tokio::test]