diff --git a/Cargo.toml b/Cargo.toml index bc2ef54b..ffda8a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,13 @@ members = [ "ucan", "ucan-key-support", + "ucan-wasm" ] # Speedup build on macOS # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information [profile.dev] split-debuginfo = "unpacked" + +[profile.release.package.ucan-wasm] +opt-level = "z" diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml index c456bd16..b5a3bf80 100644 --- a/ucan-key-support/Cargo.toml +++ b/ucan-key-support/Cargo.toml @@ -25,8 +25,8 @@ async-trait = "0.1" bs58 = "0.4" ed25519-zebra = "3.1" log = "0.4" -rsa = "0.8" p256 = "0.13" +rsa = "0.8" sha2 = { version = "0.10", features = ["oid"] } ucan = { path = "../ucan", version = "0.3.2" } diff --git a/ucan-wasm/.gitignore b/ucan-wasm/.gitignore new file mode 100644 index 00000000..fa15686a --- /dev/null +++ b/ucan-wasm/.gitignore @@ -0,0 +1,6 @@ +package-lock.json +node_modules/ +pkg/ +tests/report +.nyc_output/ +.wireit/ diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml new file mode 100644 index 00000000..08817624 --- /dev/null +++ b/ucan-wasm/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "ucan-wasm" +version = "0.2.0" +description = "A UCAN library built from rs-ucan" +keywords = ["authorization"] +categories = [] +include = ["/src", "README.md", "LICENSE"] +license = "Apache-2.0" +readme = "README.md" +edition = "2021" +rust-version = "1.64" +documentation = "https://docs.rs/rust-template-test-wasm" +repository = "https://github.com/bgins/template-test/tree/main/rust-template-test-wasm" + +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[dependencies] +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +base64 = "0.21" +cid = "0.10" +console_error_panic_hook = { version = "0.1", optional = true } +instant = { version = "0.1", features = ["wasm-bindgen"] } +js-sys = { version = "0.3", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.5.0" +serde_json = "1.0" +tracing = "0.1" +ucan = { path = "../ucan", version = "0.3" } +ucan-key-support = { path = "../ucan-key-support", version = "0.1.5" } +wasm-bindgen = { version = "0.2", optional = true, features = ["serde-serialize"] } +wasm-bindgen-futures = { version = "0.4", optional = true } +web-sys = { version = "0.3", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[features] +default = ["js"] +full = ["js", "web"] +js = [ + "console_error_panic_hook", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures" +] +web = ["web-sys"] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/ucan-wasm/LICENSE b/ucan-wasm/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/ucan-wasm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ucan-wasm/README.md b/ucan-wasm/README.md new file mode 100644 index 00000000..bf65a8b2 --- /dev/null +++ b/ucan-wasm/README.md @@ -0,0 +1,28 @@ +### Build + +The build command output for `browser`, `node`, `deno`, and `workerd` targets to `ucan-wasm/lib`. + +``` +npm run build +``` + +### Test + +The test command tests `node` and browser environments including `chromium`, `firefox`, and `webkit` +using headless browsers. + +``` +npm run test +``` + +Testing can also be run for `node` only. + +``` +npm run test:node +``` + +The test commands runs `build` to ensure all targets are available for testing. + +### Reporting + +Test runs output test results and coverage as JSON artifacts to `ucan-wasm/tests/report`. diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json new file mode 100644 index 00000000..dce7b879 --- /dev/null +++ b/ucan-wasm/package.json @@ -0,0 +1,209 @@ +{ + "name": "ucan-wasm", + "version": "0.3.0", + "description": "A UCAN library built from rs-ucan", + "repository": { + "type": "git", + "url": "git+https://github.com/ucan-wg/rs-ucan.git" + }, + "keywords": [ + "authorization" + ], + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/ucan-wg/rs-ucan/issues" + }, + "homepage": "https://github.com/ucan-wg/rs-ucan#readme", + "type": "module", + "exports": { + "./package.json": "./package.json", + ".": { + "browser": { + "types": "./lib/browser/ucan_wasm.d.ts", + "import": "./lib/browser/ucan_wasm.js" + }, + "node": { + "types": "./lib/node/ucan_wasm.d.ts", + "require": "./lib/node/ucan_wasm.js", + "import": "./lib/browser/ucan_wasm.js" + }, + "deno": { + "types": "./lib/deno/ucan_wasm.d.ts", + "import": "./lib/deno/ucan_wasm.js" + }, + "workerd": { + "types": "./lib/workerd/ucan_wasm.d.ts", + "import": "./lib/workerd/index.js" + }, + "import": "./lib/browser/ucan_wasm.js", + "require": "./lib/node/ucan_wasm.js" + } + }, + "scripts": { + "build": "export PROFILE=dev && export TARGET_DIR=debug && npm run buildall", + "release": "export PROFILE=release && export TARGET_DIR=release && npm run buildall", + "buildall": "wireit", + "clean": "wireit", + "test": "wireit", + "test:browser": "wireit", + "test:node": "wireit" + }, + "wireit": { + "compile": { + "command": "cargo build --target wasm32-unknown-unknown --profile $PROFILE", + "env": { + "PROFILE": { + "external": true + } + } + }, + "opt": { + "command": "wasm-opt -O1 ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm -o ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "compile" + ] + }, + "bindgen:browser": { + "command": "wasm-bindgen --target web --out-dir lib/browser ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:node": { + "command": "wasm-bindgen --target nodejs --out-dir lib/node ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:deno": { + "command": "wasm-bindgen --target deno --out-dir lib/deno ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:workerd": { + "command": "wasm-bindgen --target web --out-dir lib/workerd ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm && shx cp src/loaders/export-workerd-wasm.js lib/workerd/index.js", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "buildall": { + "dependencies": [ + "bindgen:browser", + "bindgen:node", + "bindgen:deno", + "bindgen:workerd" + ] + }, + "clean": { + "command": "shx rm -rf ./lib" + }, + "test:prepare": { + "command": "npx playwright install && shx mkdir tests/report", + "output": [ + "tests/report" + ] + }, + "test:chromium": { + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --cov > tests/report/chromium.json", + "dependencies": [ + "build", + "test:prepare" + ] + }, + "test:firefox": { + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --browser firefox > tests/report/firefox.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/firefox.json" + ] + }, + "test:webkit": { + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --browser webkit > tests/report/webkit.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/webkit.json" + ] + }, + "test:browser": { + "dependencies": [ + "test:chromium", + "test:firefox", + "test:webkit" + ] + }, + "test:node": { + "command": "vitest run node.test.ts --outputFile tests/report/node.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/node.json" + ] + }, + "test:report": { + "command": "nyc report --reporter=json-summary --report-dir tests/report", + "dependencies": [ + "test:chromium" + ], + "output": [ + "tests/report/coverage-summary.json" + ] + }, + "test": { + "dependencies": [ + "test:browser", + "test:node", + "test:report" + ] + } + }, + "devDependencies": { + "@playwright/test": "^1.34.3", + "@types/expect": "^24.3.0", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", + "assert": "^2.0.0", + "expect": "^29.5.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "playwright-test": "^9.1.0", + "shx": "^0.3.4", + "ts-node": "^10.9.1", + "vitest": "^0.31.4", + "wireit": "^0.9.5" + } +} diff --git a/ucan-wasm/src/lib.rs b/ucan-wasm/src/lib.rs new file mode 100644 index 00000000..3fdb5225 --- /dev/null +++ b/ucan-wasm/src/lib.rs @@ -0,0 +1,50 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(unreachable_pub, private_in_public)] + +//! ucan-wasm + +use wasm_bindgen::prelude::wasm_bindgen; + +pub mod ucan; + +//------------------------------------------------------------------------------ +// Utilities +//------------------------------------------------------------------------------ + +/// Panic hook lets us get better error messages if our Rust code ever panics. +/// +/// For more details see +/// +#[wasm_bindgen(js_name = "setPanicHook")] +pub fn set_panic_hook() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +extern "C" { + // For alerting + pub(crate) fn alert(s: &str); + // For logging in the console. + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} + +//------------------------------------------------------------------------------ +// Macros +//------------------------------------------------------------------------------ + +/// Return a representation of an object owned by JS. +#[macro_export] +macro_rules! value { + ($value:expr) => { + wasm_bindgen::JsValue::from($value) + }; +} + +/// Calls the wasm_bindgen console.log. +#[macro_export] +macro_rules! console_log { + ($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string())) +} diff --git a/ucan-wasm/src/loaders/export-workerd-wasm.js b/ucan-wasm/src/loaders/export-workerd-wasm.js new file mode 100644 index 00000000..29811960 --- /dev/null +++ b/ucan-wasm/src/loaders/export-workerd-wasm.js @@ -0,0 +1,6 @@ +// This entry point is inserted into ./lib/workerd to support Cloudflare workers + +import WASM from "./ucan_wasm_bg.wasm"; +import { initSync } from "./ucan_wasm.js"; +initSync(WASM); +export * from "./ucan_wasm.js"; diff --git a/ucan-wasm/src/ucan/cid.rs b/ucan-wasm/src/ucan/cid.rs new file mode 100644 index 00000000..909bbe93 --- /dev/null +++ b/ucan-wasm/src/ucan/cid.rs @@ -0,0 +1,43 @@ +use crate::ucan::JsResult; +use ::ucan::Ucan; +use cid::multihash::Code; +use js_sys::Error; +use wasm_bindgen::prelude::wasm_bindgen; + +/// Compute CID for a UCAN +/// +/// Hashers include SHA2-256, SHA2-512, SHA3-224 +/// SHA3-256, SHA3-384, SHA3-512, Keccak-224, Keccak-256, Keccak-384 +/// Keccak-512, BLAKE2b-256, BLAKE2b-512, BLAKE2s-128, and BLAKE3-256. +/// +/// Defaults to BLAKE3-256 hash function. +/// +#[wasm_bindgen(js_name = "toCID")] +pub async fn to_cid(token: String, hasher: Option) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + let hasher_code = get_hasher_code(&hasher.unwrap_or(String::from("BLAKE3-256"))); + let cid = Ucan::to_cid(&ucan, hasher_code).map_err(|e| Error::new(&format!("{e}")))?; + + Ok(cid.into()) +} + +fn get_hasher_code(hasher: &str) -> Code { + match hasher { + "SHA2-256" => Code::Sha2_256, + "SHA2-512" => Code::Sha2_512, + "SHA3-224" => Code::Sha3_224, + "SHA3-256" => Code::Sha3_256, + "SHA3-384" => Code::Sha3_384, + "SHA3-512" => Code::Sha3_512, + "Keccak-224" => Code::Keccak224, + "Keccak-256" => Code::Keccak256, + "Keccak-384" => Code::Keccak384, + "Keccak-512" => Code::Keccak512, + "BLAKE2b-256" => Code::Blake2b256, + "BLAKE2b-512" => Code::Blake2b512, + "BLAKE2s-128" => Code::Blake2s128, + "BLAKE3-256" => Code::Blake3_256, + _ => Code::Blake3_256, + } +} diff --git a/ucan-wasm/src/ucan/mod.rs b/ucan-wasm/src/ucan/mod.rs new file mode 100644 index 00000000..e63e2c2a --- /dev/null +++ b/ucan-wasm/src/ucan/mod.rs @@ -0,0 +1,5 @@ +pub mod cid; +pub mod token; +pub mod verify; + +pub type JsResult = Result; diff --git a/ucan-wasm/src/ucan/token.rs b/ucan-wasm/src/ucan/token.rs new file mode 100644 index 00000000..8761a4c2 --- /dev/null +++ b/ucan-wasm/src/ucan/token.rs @@ -0,0 +1,101 @@ +use crate::ucan::JsResult; +use ::ucan::{capability::CapabilityIpld, Ucan as RsUcan}; +use base64::Engine; +use js_sys::Error; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_wasm_bindgen::Serializer; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const UCAN: &'static str = r#" +interface Ucan { + header: { + alg: string, + typ: string, + ucv: string + }, + payload: { + iss: string, + aud: string, + exp: number, + nbf?: number, + nnc?: string, + att: unknown[], + fct?: Record[], + prf?: string[] + } + signature: string +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Ucan")] + pub type Ucan; +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct ResolvedUcan { + header: Header, + payload: Payload, + signature: String, +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct Header { + alg: String, + typ: String, + ucv: String, +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + iss: String, + aud: String, + exp: u64, + nbf: Option, + nnc: Option, + att: Vec, + fct: Option>, + prf: Option>, +} + +/// Decode a UCAN +#[wasm_bindgen(js_name = "decode")] +pub async fn decode(token: String) -> JsResult { + let ucan = RsUcan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + let header = Header { + alg: ucan.algorithm().into(), + typ: "JWT".into(), + ucv: ucan.version().into(), + }; + + let payload = Payload { + iss: ucan.issuer().into(), + aud: ucan.audience().into(), + exp: *ucan.expires_at(), + nbf: *ucan.not_before(), + nnc: ucan.nonce().clone(), + att: ucan.attenuation().to_vec(), + fct: ucan.facts().clone(), + prf: ucan.proofs().clone(), + }; + + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(ucan.signature()); + + let resolved = ResolvedUcan { + header, + payload, + signature, + }; + + let serializer = Serializer::new().serialize_maps_as_objects(true); + let value = resolved.serialize(&serializer).unwrap(); + + Ok(Ucan { obj: value }) +} diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs new file mode 100644 index 00000000..27f262b0 --- /dev/null +++ b/ucan-wasm/src/ucan/verify.rs @@ -0,0 +1,63 @@ +use crate::ucan::JsResult; +use ::ucan::{ + crypto::did::{ + DidParser, KeyConstructorSlice, ED25519_MAGIC_BYTES, P256_MAGIC_BYTES, RSA_MAGIC_BYTES, + }, + time::now, + Ucan, +}; +use ::ucan_key_support::{ + ed25519::bytes_to_ed25519_key, p256::bytes_to_p256_key, rsa::bytes_to_rsa_key, +}; +use js_sys::Error; +use wasm_bindgen::prelude::wasm_bindgen; + +const SUPPORTED_KEYS: &KeyConstructorSlice = &[ + (ED25519_MAGIC_BYTES, bytes_to_ed25519_key), + (RSA_MAGIC_BYTES, bytes_to_rsa_key), + (P256_MAGIC_BYTES, bytes_to_p256_key), +]; + +/// Validate the UCAN's signature and timestamps +#[wasm_bindgen(js_name = "validate")] +pub async fn validate(token: String) -> JsResult<()> { + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + let now = now(); + + Ucan::validate(&ucan, Some(now), &mut did_parser) + .await + .map_err(|e| Error::new(&format!("{e}")))?; + + Ok(()) +} + +/// Validate that the signed data was signed by the stated issuer +#[wasm_bindgen(js_name = "checkSignature")] +pub async fn check_signature(token: String) -> JsResult<()> { + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + ucan.check_signature(&mut did_parser) + .await + .map_err(|e| Error::new(&format!("{e}")))?; + + Ok(()) +} + +/// Returns true if the UCAN has past its expiration date +#[wasm_bindgen(js_name = "isExpired")] +pub fn is_expired(token: String) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + let now = now(); + + Ok(Ucan::is_expired(&ucan, Some(now))) +} + +/// Returns true if the not-before ("nbf") time is still in the future +#[wasm_bindgen(js_name = "isTooEarly")] +pub fn is_too_early(token: String) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + Ok(Ucan::is_too_early(&ucan)) +} diff --git a/ucan-wasm/tests/browser.test.ts b/ucan-wasm/tests/browser.test.ts new file mode 100644 index 00000000..941bc24f --- /dev/null +++ b/ucan-wasm/tests/browser.test.ts @@ -0,0 +1,31 @@ +import init, { checkSignature, decode, isExpired, isTooEarly, toCID, validate } from '../lib/browser/ucan_wasm.js' +import { runCIDTests } from "./ucan/cid.test.js" +import { runTokenTests } from "./ucan/token.test.js" +import { runVerifyTests } from "./ucan/verify.test.js" + +before(async () => { + await init() +}) + +runVerifyTests({ + ucan: { + isExpired, + isTooEarly, + checkSignature, + validate + } +}) + + +runTokenTests({ + ucan: { + decode + } +}) + +runCIDTests({ + runner: { describe, it }, + ucan: { + toCID + } +}) diff --git a/ucan-wasm/tests/fixtures/cid.json b/ucan-wasm/tests/fixtures/cid.json new file mode 100644 index 00000000..8c8e0f0b --- /dev/null +++ b/ucan-wasm/tests/fixtures/cid.json @@ -0,0 +1,13 @@ + +[ + { + "hasher": "SHA2-256", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "cid": "bafkreigzlstpwdjhxgjexfm3njsidz3iusyys3d2way35re3gbcghjzsqa" + }, + { + "hasher": "BLAKE3-256", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "cid": "bafkr4igg7afrwmgzugre2zxd62t3ycs73hto3pe24bavabroxbjpfam57y" + } +] diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts new file mode 100644 index 00000000..c380609d --- /dev/null +++ b/ucan-wasm/tests/fixtures/index.ts @@ -0,0 +1,58 @@ +import cid from './cid.json' +import invalid from './invalid.json' +import valid from './valid.json' + + +// VERIFICATION + +type Expectation = 'valid' | 'invalid' + +type Fixture = { + comment: string + token: string + assertions: { + header: { + alg: "EdDSA" + typ: "JWT" + ucv: "0.9.0-canary" + }, + payload: { + iss: string + aud: string + exp: number | null + nbf?: number + nnc?: string + fct: Record[], + att: { with: string, can: string }[], + prf: string[] + }, + signature: string, + validationErrors?: string[] + }, +} + + +export function getFixture(expectation: Expectation, comment: string): Fixture { + let fixture + + if (expectation === 'valid') { + fixture = valid.find(f => f.comment === comment) + } else if (expectation === 'invalid') { + fixture = invalid.find(f => f.comment === comment) + } + + return fixture +} + + +// CID + +type CIDFixture = { + hasher: string + token: string + cid: string +} + +export function getCIDFixture(hasher: string): CIDFixture { + return cid.find(f => f.hasher === hasher) +} diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json new file mode 100644 index 00000000..6b6a0a09 --- /dev/null +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -0,0 +1,72 @@ +[ + { + "comment": "UCAN has expired", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6MSwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 1, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ", + "validationErrors": [ + "expExpired" + ] + } + }, + { + "comment": "UCAN is not ready to be used", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjkyNDYwMDAwMDAsInByZiI6W119.W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "nbf": 9246000000, + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "validationErrors": [ + "nbfNotReady" + ] + } + }, + { + "comment": "UCAN has an invalid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "validationErrors": [ + "invalidSignature" + ] + } + } +] diff --git a/ucan-wasm/tests/fixtures/valid.json b/ucan-wasm/tests/fixtures/valid.json new file mode 100644 index 00000000..881b0ce1 --- /dev/null +++ b/ucan-wasm/tests/fixtures/valid.json @@ -0,0 +1,95 @@ +[ + { + "comment": "UCAN is valid", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": 9246211200, + "fct": [ + { + "challenge": "abcdef" + } + ], + "att": [ + { + "can": "email/send", + "nb": null, + "with": "mailto:alice@email.com" + } + ], + "prf": [ + "bafkr4igf3sz7kj5dhxrdjufxvavkkinpk2i73i5psup6cxuvdym2jecw2e" + ] + }, + "signature": "nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg" + } + }, + { + "comment": "UCAN has not expired", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ" + } + }, + { + "comment": "UCAN is ready to be used", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInByZiI6W119.-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "nbf": 1, + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw" + } + }, + { + "comment": "UCAN has a valid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ" + } + } +] diff --git a/ucan-wasm/tests/node.test.ts b/ucan-wasm/tests/node.test.ts new file mode 100644 index 00000000..fab3d904 --- /dev/null +++ b/ucan-wasm/tests/node.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from 'vitest' + +import { checkSignature, decode, isExpired, isTooEarly, toCID, validate } from '../lib/node/ucan_wasm.js' +import { runCIDTests } from "./ucan/cid.test.js" +import { runTokenTests } from "./ucan/token.test.js" +import { runVerifyTests } from "./ucan/verify.test.js" + +runVerifyTests({ + runner: { describe, it }, + ucan: { + isExpired, + isTooEarly, + checkSignature, + validate + } +}) + +runTokenTests({ + runner: { describe, it }, + ucan: { + decode + } +}) + +runCIDTests({ + runner: { describe, it }, + ucan: { + toCID + } +}) diff --git a/ucan-wasm/tests/ucan/cid.test.ts b/ucan-wasm/tests/ucan/cid.test.ts new file mode 100644 index 00000000..a0b5ab13 --- /dev/null +++ b/ucan-wasm/tests/ucan/cid.test.ts @@ -0,0 +1,37 @@ +import assert from 'assert' +import { getCIDFixture } from '../fixtures/index.js' + +export function runCIDTests( + impl: { + runner?: { describe, it }, + ucan: { + toCID: (token: string, hasher?: string) => Promise + } + }) { + + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { toCID } = impl.ucan + + describe('toCID', async () => { + it('should compute CID for a UCAN using a SHA2-256 hasher', async () => { + const fixture = getCIDFixture('SHA2-256') + const cid = await toCID(fixture.token, 'SHA2-256') + assert.equal(cid, fixture.cid) + }) + + it('should compute CID for a UCAN using a BLAKE3-256 hasher', async () => { + const fixture = getCIDFixture('BLAKE3-256') + const cid = await toCID(fixture.token, 'BLAKE3-256') + assert.equal(cid, fixture.cid) + }) + + it('should compute CID for a UCAN deafulting to the BLAKE3-256 hasher', async () => { + const fixture = getCIDFixture('BLAKE3-256') + const cid = await toCID(fixture.token) + assert.equal(cid, fixture.cid) + }) + }) +} diff --git a/ucan-wasm/tests/ucan/token.test.ts b/ucan-wasm/tests/ucan/token.test.ts new file mode 100644 index 00000000..839b41dc --- /dev/null +++ b/ucan-wasm/tests/ucan/token.test.ts @@ -0,0 +1,46 @@ +import assert from 'assert' +import { getFixture } from '../fixtures/index.js' + +// The Ucan type is the same across browser and node environments +import type { Ucan } from '../../lib/browser/ucan_wasm.js' + +export function runTokenTests( + impl: { + runner?: { describe, it }, + ucan: { + decode: (token: string) => Promise + } + }) { + + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { decode } = impl.ucan + + describe('decode', async () => { + it('should decode a token', async () => { + const valid = getFixture('valid', 'UCAN is valid') + const ucan = await decode(valid.token) + + // Check header + assert.equal(ucan.header.alg, valid.assertions.header.alg) + assert.equal(ucan.header.typ, valid.assertions.header.typ) + assert.equal(ucan.header.ucv, valid.assertions.header.ucv) + + // Check payload + assert.equal(ucan.payload.iss, valid.assertions.payload.iss) + assert.equal(ucan.payload.aud, valid.assertions.payload.aud) + assert.equal(ucan.payload.exp, valid.assertions.payload.exp) + assert.equal(ucan.payload.nbf, valid.assertions.payload.nbf) + assert.equal(ucan.payload.nnc, valid.assertions.payload.nnc) + assert.deepEqual(ucan.payload.att, valid.assertions.payload.att) + assert.deepEqual(ucan.payload.fct, valid.assertions.payload.fct) + assert.deepEqual(ucan.payload.prf, valid.assertions.payload.prf) + + // Check signature + assert.equal(ucan.signature, valid.assertions.signature) + }) + }) + +} diff --git a/ucan-wasm/tests/ucan/verify.test.ts b/ucan-wasm/tests/ucan/verify.test.ts new file mode 100644 index 00000000..1037ce9e --- /dev/null +++ b/ucan-wasm/tests/ucan/verify.test.ts @@ -0,0 +1,78 @@ +import assert from 'assert' +import { getFixture } from '../fixtures/index.js' + +export function runVerifyTests( + impl: { + runner?: { describe, it }, + ucan: { + isExpired: (token: string) => boolean + isTooEarly: (token: string) => boolean + checkSignature: (token: string) => Promise + validate: (token: string) => Promise + } + }) { + + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { checkSignature, isExpired, isTooEarly, validate } = impl.ucan + + describe('validate', async () => { + it('should resolve on a valid a UCAN', async () => { + const validSignature = getFixture('valid', 'UCAN has a valid signature') + const unexpired = getFixture('valid', 'UCAN has not expired') + const active = getFixture('valid', 'UCAN is ready to be used') + + await assert.doesNotReject(validate(validSignature.token)) + await assert.doesNotReject(validate(unexpired.token)) + await assert.doesNotReject(validate(active.token)) + }) + + it('should be true when a UCAN is expired', async () => { + const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') + const expired = getFixture('invalid', 'UCAN has expired') + const early = getFixture('invalid', 'UCAN is not ready to be used') + + await assert.rejects(validate(invalidSignature.token)) + await assert.rejects(validate(expired.token)) + await assert.rejects(validate(early.token)) + }) + }) + + describe('checkSignature', async () => { + it('should resolve on a valid signature', async () => { + const valid = getFixture('valid', 'UCAN has a valid signature') + await assert.doesNotReject(checkSignature(valid.token)) + }) + + it('should throw on an invalid signature', async () => { + const invalid = getFixture('invalid', 'UCAN has an invalid signature') + await assert.rejects(checkSignature(invalid.token)) + }) + }) + + describe('isExpired', () => { + it('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN has not expired') + assert.equal(isExpired(valid.token), false) + }) + + it('should be true when a UCAN is expired', () => { + const invalid = getFixture('invalid', 'UCAN has expired') + assert(isExpired(invalid.token)) + }) + }) + + describe('isTooEarly', () => { + it('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN is ready to be used') + assert.equal(isTooEarly(valid.token), false) + }) + + it('should be true when a UCAN is early', () => { + const invalid = getFixture('invalid', 'UCAN is not ready to be used') + assert(isTooEarly(invalid.token)) + }) + }) +} diff --git a/ucan-wasm/tsconfig.json b/ucan-wasm/tsconfig.json new file mode 100644 index 00000000..a8b91537 --- /dev/null +++ b/ucan-wasm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": ["tests/**/*.test.ts"] +} diff --git a/ucan-wasm/vitest.config.ts b/ucan-wasm/vitest.config.ts new file mode 100644 index 00000000..97ca8107 --- /dev/null +++ b/ucan-wasm/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + reporters: [ "json" ] + }, +})