From 18d9a41dc7aa9823bca315255f0aefb1ba2ceebf Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 6 Jun 2023 18:56:23 -0700 Subject: [PATCH] feat: add decode function and test --- ucan-wasm/Cargo.toml | 4 + ucan-wasm/src/ucan/mod.rs | 1 + ucan-wasm/src/ucan/token.rs | 101 ++++++++++++++++++++++++++ ucan-wasm/src/ucan/verify.rs | 1 - ucan-wasm/tests/browser.test.ts | 10 ++- ucan-wasm/tests/fixtures/index.ts | 8 +- ucan-wasm/tests/fixtures/invalid.json | 10 ++- ucan-wasm/tests/fixtures/valid.json | 41 ++++++++++- ucan-wasm/tests/node.test.ts | 10 ++- ucan-wasm/tests/ucan/token.test.ts | 46 ++++++++++++ ucan-wasm/tests/ucan/verify.test.ts | 1 + 11 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 ucan-wasm/src/ucan/token.rs create mode 100644 ucan-wasm/tests/ucan/token.test.ts diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml index b9585bb8..2f4792f9 100644 --- a/ucan-wasm/Cargo.toml +++ b/ucan-wasm/Cargo.toml @@ -21,9 +21,13 @@ path = "src/lib.rs" # 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" 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" } diff --git a/ucan-wasm/src/ucan/mod.rs b/ucan-wasm/src/ucan/mod.rs index 6bfa815b..8770866f 100644 --- a/ucan-wasm/src/ucan/mod.rs +++ b/ucan-wasm/src/ucan/mod.rs @@ -1,3 +1,4 @@ +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 index ad42a6e2..27f262b0 100644 --- a/ucan-wasm/src/ucan/verify.rs +++ b/ucan-wasm/src/ucan/verify.rs @@ -9,7 +9,6 @@ use ::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; diff --git a/ucan-wasm/tests/browser.test.ts b/ucan-wasm/tests/browser.test.ts index 0ea10e19..11abd640 100644 --- a/ucan-wasm/tests/browser.test.ts +++ b/ucan-wasm/tests/browser.test.ts @@ -1,5 +1,6 @@ -import init, { checkSignature, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' +import init, { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' import { runVerifyTests } from "./ucan/verify.test.js" +import { runTokenTests } from "./ucan/token.test.js" before(async () => { await init() @@ -13,3 +14,10 @@ runVerifyTests({ validate } }) + + +runTokenTests({ + ucan: { + decode + } +}) diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts index 709f5389..f626e11c 100644 --- a/ucan-wasm/tests/fixtures/index.ts +++ b/ucan-wasm/tests/fixtures/index.ts @@ -18,13 +18,13 @@ type Fixture = { exp: number | null nbf?: number nnc?: string - fct: Record[], - att: {with: string, can: string}[], + fct: Record[], + att: { with: string, can: string }[], prf: string[] }, + signature: string, validationErrors?: string[] - } - + }, } export function getFixture(expectation: Expectation, comment: string): Fixture { diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json index 247b14da..6b6a0a09 100644 --- a/ucan-wasm/tests/fixtures/invalid.json +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -16,6 +16,7 @@ "att": [], "prf": [] }, + "signature": "berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ", "validationErrors": [ "expExpired" ] @@ -39,6 +40,7 @@ "att": [], "prf": [] }, + "signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", "validationErrors": [ "nbfNotReady" ] @@ -46,7 +48,7 @@ }, { "comment": "UCAN has an invalid signature", - "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglC", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", "assertions": { "header": { "alg": "EdDSA", @@ -60,7 +62,11 @@ "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 index 022d964b..881b0ce1 100644 --- a/ucan-wasm/tests/fixtures/valid.json +++ b/ucan-wasm/tests/fixtures/valid.json @@ -1,4 +1,36 @@ [ + { + "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", @@ -15,7 +47,8 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ" } }, { @@ -35,7 +68,8 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw" } }, { @@ -54,7 +88,8 @@ "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 index 51c886c5..0dcdc176 100644 --- a/ucan-wasm/tests/node.test.ts +++ b/ucan-wasm/tests/node.test.ts @@ -1,6 +1,7 @@ import { describe, it } from 'vitest' -import { checkSignature, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { runTokenTests } from "./ucan/token.test.js" import { runVerifyTests } from "./ucan/verify.test.js" runVerifyTests({ @@ -12,3 +13,10 @@ runVerifyTests({ validate } }) + +runTokenTests({ + runner: { describe, it }, + ucan: { + decode + } +}) 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 index 32d9ca76..1037ce9e 100644 --- a/ucan-wasm/tests/ucan/verify.test.ts +++ b/ucan-wasm/tests/ucan/verify.test.ts @@ -11,6 +11,7 @@ export function runVerifyTests( 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