Skip to content

Commit

Permalink
feat: add decode function and test
Browse files Browse the repository at this point in the history
  • Loading branch information
bgins committed Jun 7, 2023
1 parent 83deb1e commit 18d9a41
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 12 deletions.
4 changes: 4 additions & 0 deletions ucan-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions ucan-wasm/src/ucan/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod token;
pub mod verify;

pub type JsResult<T> = Result<T, js_sys::Error>;
101 changes: 101 additions & 0 deletions ucan-wasm/src/ucan/token.rs
Original file line number Diff line number Diff line change
@@ -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<string,unknown>[],
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<u64>,
nnc: Option<String>,
att: Vec<CapabilityIpld>,
fct: Option<Vec<Value>>,
prf: Option<Vec<String>>,
}

/// Decode a UCAN
#[wasm_bindgen(js_name = "decode")]
pub async fn decode(token: String) -> JsResult<Ucan> {
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 })
}
1 change: 0 additions & 1 deletion ucan-wasm/src/ucan/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
10 changes: 9 additions & 1 deletion ucan-wasm/tests/browser.test.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -13,3 +14,10 @@ runVerifyTests({
validate
}
})


runTokenTests({
ucan: {
decode
}
})
8 changes: 4 additions & 4 deletions ucan-wasm/tests/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ type Fixture = {
exp: number | null
nbf?: number
nnc?: string
fct: Record<string,unknown>[],
att: {with: string, can: string}[],
fct: Record<string, unknown>[],
att: { with: string, can: string }[],
prf: string[]
},
signature: string,
validationErrors?: string[]
}

},
}

export function getFixture(expectation: Expectation, comment: string): Fixture {
Expand Down
10 changes: 8 additions & 2 deletions ucan-wasm/tests/fixtures/invalid.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"att": [],
"prf": []
},
"signature": "berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ",
"validationErrors": [
"expExpired"
]
Expand All @@ -39,14 +40,15 @@
"att": [],
"prf": []
},
"signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg",
"validationErrors": [
"nbfNotReady"
]
}
},
{
"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",
Expand All @@ -60,7 +62,11 @@
"fct": [],
"att": [],
"prf": []
}
},
"signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg",
"validationErrors": [
"invalidSignature"
]
}
}
]
41 changes: 38 additions & 3 deletions ucan-wasm/tests/fixtures/valid.json
Original file line number Diff line number Diff line change
@@ -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:[email protected]"
}
],
"prf": [
"bafkr4igf3sz7kj5dhxrdjufxvavkkinpk2i73i5psup6cxuvdym2jecw2e"
]
},
"signature": "nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg"
}
},
{
"comment": "UCAN has not expired",
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ",
Expand All @@ -15,7 +47,8 @@
"fct": [],
"att": [],
"prf": []
}
},
"signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ"
}
},
{
Expand All @@ -35,7 +68,8 @@
"fct": [],
"att": [],
"prf": []
}
},
"signature": "-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw"
}
},
{
Expand All @@ -54,7 +88,8 @@
"fct": [],
"att": [],
"prf": []
}
},
"signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ"
}
}
]
10 changes: 9 additions & 1 deletion ucan-wasm/tests/node.test.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -12,3 +13,10 @@ runVerifyTests({
validate
}
})

runTokenTests({
runner: { describe, it },
ucan: {
decode
}
})
46 changes: 46 additions & 0 deletions ucan-wasm/tests/ucan/token.test.ts
Original file line number Diff line number Diff line change
@@ -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<Ucan>
}
}) {

// 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)
})
})

}
1 change: 1 addition & 0 deletions ucan-wasm/tests/ucan/verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function runVerifyTests(
validate: (token: string) => Promise<void>
}
}) {

// Use runner or fallback to implicit mocha implementations
const describe = impl.runner?.describe ?? globalThis.describe
const it = impl.runner?.it ?? globalThis.it
Expand Down

0 comments on commit 18d9a41

Please sign in to comment.