diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml index 22062981..2d558b23 100644 --- a/ucan-key-support/Cargo.toml +++ b/ucan-key-support/Cargo.toml @@ -1,34 +1,37 @@ [package] -name = "ucan-key-support" -description = "Ready to use SigningKey implementations for the ucan crate" -edition = "2021" -keywords = ["ucan", "authz", "jwt", "pki"] categories = [ "authorization", "cryptography", "encoding", - "web-programming" + "web-programming", ] +description = "Ready to use SigningKey implementations for the ucan crate" documentation = "https://docs.rs/ucan" -repository = "https://github.com/cdata/rs-ucan/" +edition = "2021" homepage = "https://github.com/cdata/rs-ucan" +keywords = ["ucan", "authz", "jwt", "pki"] license = "Apache-2.0" +name = "ucan-key-support" readme = "README.md" +repository = "https://github.com/cdata/rs-ucan/" version = "0.4.0-alpha.1" +[lib] +crate-type = ["cdylib", "rlib"] + [features] default = [] web = ["wasm-bindgen", "wasm-bindgen-futures", "js-sys", "web-sys", "ucan/web", "getrandom/js"] [dependencies] -ucan = {path = "../ucan", version = "0.6.0-alpha.1" } anyhow = "1.0.52" async-trait = "0.1.52" +bs58 = "0.4" ed25519-zebra = "^3" +log = "0.4" rsa = "0.6" sha2 = "0.10" -bs58 = "0.4" -log = "0.4" +ucan = {path = "../ucan", version = "0.6.0-alpha.1"} [build-dependencies] npm_rs = "0.2.1" @@ -36,26 +39,27 @@ npm_rs = "0.2.1" [dev-dependencies] rand = "0.8" # NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "0.2.5", features = ["js"] } +getrandom = {version = "0.2.5", features = ["js"]} +tokio = {version = "^1", features = ["macros", "rt"]} wasm-bindgen-test = "0.3" -tokio = { version = "^1", features = ["macros", "rt"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { version = "0.2", optional = true } -wasm-bindgen-futures = { version = "0.4", optional = true } -js-sys = { version = "0.3", optional = true } +js-sys = {version = "0.3", optional = true} +wasm-bindgen = {version = "0.2", features = ["serde-serialize"], optional = true} +wasm-bindgen-futures = {version = "0.4", optional = true} +wee_alloc = {version = "0.4"} [target.'cfg(target_arch="wasm32")'.dependencies.web-sys] -version = "0.3" -optional = true features = [ 'Window', 'SubtleCrypto', 'Crypto', 'CryptoKey', 'CryptoKeyPair', - 'DedicatedWorkerGlobalScope' + 'DedicatedWorkerGlobalScope', ] +optional = true +version = "0.3" [target.'cfg(target_arch="wasm32")'.dev-dependencies] -pollster = "0.2.5" \ No newline at end of file +pollster = "0.2.5" diff --git a/ucan-key-support/demo/.gitignore b/ucan-key-support/demo/.gitignore new file mode 100644 index 00000000..93a464bb --- /dev/null +++ b/ucan-key-support/demo/.gitignore @@ -0,0 +1 @@ +static/ \ No newline at end of file diff --git a/ucan-key-support/demo/build.sh b/ucan-key-support/demo/build.sh new file mode 100755 index 00000000..c589ae30 --- /dev/null +++ b/ucan-key-support/demo/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cargo build --release --target="wasm32-unknown-unknown" --features=web + +wasm-bindgen \ + --target web \ + --out-dir static \ + ../../target/wasm32-unknown-unknown/release/ucan_key_support.wasm diff --git a/ucan-key-support/demo/index.html b/ucan-key-support/demo/index.html new file mode 100644 index 00000000..07d3e6e3 --- /dev/null +++ b/ucan-key-support/demo/index.html @@ -0,0 +1,79 @@ + + + + UCANs on the Web - Powered by rs-ucan + + + + + + +

+  
+
diff --git a/ucan-key-support/src/lib.rs b/ucan-key-support/src/lib.rs
index 78c0e226..333ca98d 100644
--- a/ucan-key-support/src/lib.rs
+++ b/ucan-key-support/src/lib.rs
@@ -4,5 +4,9 @@ extern crate log;
 #[cfg(all(target_arch = "wasm32", feature = "web"))]
 pub mod web_crypto;
 
+#[cfg(all(target_arch = "wasm32", feature = "web"))]
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
+
 pub mod ed25519;
 pub mod rsa;
diff --git a/ucan-key-support/src/rsa.rs b/ucan-key-support/src/rsa.rs
index 39172c3a..9bb2a741 100644
--- a/ucan-key-support/src/rsa.rs
+++ b/ucan-key-support/src/rsa.rs
@@ -1,11 +1,9 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 
-use rsa::{
-    Hash, PaddingScheme, PublicKey, RsaPrivateKey, RsaPublicKey,
-};
-use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey};
 use rsa::pkcs1::der::{Document, Encodable};
+use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey};
+use rsa::{Hash, PaddingScheme, PublicKey, RsaPrivateKey, RsaPublicKey};
 
 use sha2::{Digest, Sha256};
 use ucan::crypto::KeyMaterial;
diff --git a/ucan-key-support/src/web_crypto.rs b/ucan-key-support/src/web_crypto.rs
index 3b482104..dca32484 100644
--- a/ucan-key-support/src/web_crypto.rs
+++ b/ucan-key-support/src/web_crypto.rs
@@ -1,13 +1,17 @@
+use crate::rsa::{bytes_to_rsa_key, RSA_MAGIC_BYTES};
 use crate::rsa::{RsaKeyMaterial, RSA_ALGORITHM};
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use js_sys::{Array, ArrayBuffer, Boolean, Object, Reflect, Uint8Array};
-use rsa::RsaPublicKey;
-use rsa::pkcs1::DecodeRsaPublicKey;
+use js_sys::{Array, ArrayBuffer, Boolean, Date, Object, Promise, Reflect, Uint8Array};
 use rsa::pkcs1::der::Encodable;
-use ucan::crypto::KeyMaterial;
-use wasm_bindgen::{JsCast, JsValue};
-use wasm_bindgen_futures::JsFuture;
+use rsa::pkcs1::DecodeRsaPublicKey;
+use rsa::RsaPublicKey;
+use ucan::builder::{Signable, UcanBuilder};
+use ucan::crypto::{did::DidParser, KeyMaterial};
+use ucan::ucan::Ucan;
+use wasm_bindgen::prelude::wasm_bindgen;
+use wasm_bindgen::{JsCast, JsError, JsValue};
+use wasm_bindgen_futures::{future_to_promise, JsFuture};
 use web_sys::{Crypto, CryptoKey, CryptoKeyPair, SubtleCrypto};
 
 pub fn convert_spki_to_rsa_public_key(spki_bytes: &[u8]) -> Result> {
@@ -18,8 +22,30 @@ pub fn convert_spki_to_rsa_public_key(spki_bytes: &[u8]) -> Result> {
 }
 
 #[derive(Debug)]
-pub struct WebCryptoRsaKeyMaterial(pub CryptoKey, pub Option);
+pub struct WasmError(anyhow::Error);
+
+impl From for JsValue {
+    fn from(err: WasmError) -> JsValue {
+        JsError::new(&format!("{:?}", err)).into()
+    }
+}
 
+impl From for WasmError {
+    fn from(err: anyhow::Error) -> Self {
+        Self(err)
+    }
+}
+
+type WasmResult = std::result::Result;
+
+#[derive(Clone)]
+#[wasm_bindgen]
+pub struct WebCryptoRsaKeyMaterial {
+    public_key: CryptoKey,
+    private_key: Option,
+}
+
+#[wasm_bindgen]
 impl WebCryptoRsaKeyMaterial {
     fn get_subtle_crypto() -> Result {
         // NOTE: Accessing either `Window` or `DedicatedWorkerGlobalScope` in
@@ -33,13 +59,14 @@ impl WebCryptoRsaKeyMaterial {
     }
 
     fn private_key(&self) -> Result<&CryptoKey> {
-        match &self.1 {
+        match &self.private_key {
             Some(key) => Ok(key),
             None => Err(anyhow!("No private key configured")),
         }
     }
 
-    pub async fn generate(key_size: Option) -> Result {
+    #[wasm_bindgen]
+    pub async fn generate(key_size: Option) -> WasmResult {
         let subtle_crypto = Self::get_subtle_crypto()?;
         let algorithm = Object::new();
 
@@ -98,7 +125,53 @@ impl WebCryptoRsaKeyMaterial {
                 .map_err(|error| anyhow!("{:?}", error))?,
         );
 
-        Ok(WebCryptoRsaKeyMaterial(public_key, Some(private_key)))
+        Ok(WebCryptoRsaKeyMaterial {
+            public_key,
+            private_key: Some(private_key),
+        })
+    }
+
+    #[wasm_bindgen(js_name = "getDid")]
+    pub fn wasm_get_did(&self) -> WasmResult {
+        let me = self.clone();
+
+        Ok(future_to_promise(async move {
+            let did = me.get_did().await.map_err(|err| WasmError::from(err))?;
+            Ok(JsValue::from_str(&did))
+        }))
+    }
+
+    #[wasm_bindgen(js_name = "sign")]
+    pub fn wasm_sign(&self, payload: &[u8]) -> WasmResult {
+        let me = self.clone();
+        let payload = payload.to_vec();
+
+        Ok(future_to_promise(async move {
+            let res = me
+                .sign(&payload)
+                .await
+                .map_err(|err| WasmError::from(err))?;
+            Ok(JsValue::from(Uint8Array::from(res.as_slice())))
+        }))
+    }
+
+    #[wasm_bindgen(js_name = "verify")]
+    pub fn wasm_verify(&self, payload: &[u8], signature: &[u8]) -> WasmResult {
+        let me = self.clone();
+        let payload = payload.to_vec();
+        let signature = signature.to_vec();
+
+        Ok(future_to_promise(async move {
+            me.verify(&payload, &signature)
+                .await
+                .map_err(|err| WasmError::from(err))?;
+            Ok(JsValue::UNDEFINED)
+        }))
+    }
+
+    #[wasm_bindgen(js_name = "jwtAlgorithm")]
+    pub fn wasm_jwt_algorithm(&self) -> String {
+        self.get_jwt_algorithm_name()
     }
 }
 
@@ -109,7 +182,7 @@ impl KeyMaterial for WebCryptoRsaKeyMaterial {
     }
 
     async fn get_did(&self) -> Result {
-        let public_key = &self.0;
+        let public_key = &self.public_key;
         let subtle_crypto = Self::get_subtle_crypto()?;
 
         let public_key_bytes = Uint8Array::new(
@@ -168,7 +241,7 @@ impl KeyMaterial for WebCryptoRsaKeyMaterial {
     }
 
     async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> {
-        let key = &self.0;
+        let key = &self.public_key;
         let subtle_crypto = Self::get_subtle_crypto()?;
         let algorithm = Object::new();
 
@@ -207,6 +280,245 @@ impl KeyMaterial for WebCryptoRsaKeyMaterial {
     }
 }
 
+#[wasm_bindgen]
+pub struct WasmUcan {
+    inner: Ucan,
+}
+
+#[wasm_bindgen]
+impl WasmUcan {
+    #[wasm_bindgen(js_name = "fromToken")]
+    pub fn from_token(token: &str) -> WasmResult {
+        let ucan = Ucan::try_from_token_string(token).map_err(|err| WasmError::from(err))?;
+        Ok(WasmUcan { inner: ucan })
+    }
+
+    #[wasm_bindgen]
+    pub fn validate(&self) -> WasmResult {
+        let ucan = self.inner.clone();
+
+        Ok(future_to_promise(async move {
+            let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]);
+            ucan.validate(&mut did_parser)
+                .await
+                .map_err(|err| WasmError::from(err))?;
+            Ok(JsValue::TRUE)
+        }))
+    }
+
+    #[wasm_bindgen(js_name = "checkSignature")]
+    pub fn check_signature(&self) -> WasmResult {
+        let ucan = self.inner.clone();
+
+        Ok(future_to_promise(async move {
+            let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]);
+            ucan.check_signature(&mut did_parser)
+                .await
+                .map_err(|err| WasmError::from(err))?;
+            Ok(JsValue::TRUE)
+        }))
+    }
+
+    #[wasm_bindgen]
+    pub fn encode(&self) -> WasmResult {
+        self.inner.encode().map_err(|err| WasmError::from(err))
+    }
+
+    #[wasm_bindgen(js_name = "isExpired")]
+    pub fn is_expired(&self) -> bool {
+        self.inner.is_expired()
+    }
+
+    #[wasm_bindgen(js_name = "isTooEarly")]
+    pub fn is_too_early(&self) -> bool {
+        self.inner.is_too_early()
+    }
+
+    #[wasm_bindgen(js_name = "signedData")]
+    pub fn signed_data(&self) -> Vec {
+        self.inner.signed_data().to_vec()
+    }
+
+    #[wasm_bindgen]
+    pub fn algorithm(&self) -> String {
+        self.inner.algorithm().to_string()
+    }
+
+    #[wasm_bindgen]
+    pub fn issuer(&self) -> String {
+        self.inner.issuer().to_string()
+    }
+
+    #[wasm_bindgen]
+    pub fn audience(&self) -> String {
+        self.inner.audience().to_string()
+    }
+
+    #[wasm_bindgen]
+    pub fn proofs(&self) -> Vec {
+        self.inner
+            .proofs()
+            .into_iter()
+            .map(|proof| JsValue::from_str(proof))
+            .collect()
+    }
+
+    #[wasm_bindgen]
+    pub fn expires_at(&self) -> Date {
+        // The UCAN value is the Unix Timestamp in seconds, but
+        // Date expects milliseconds since EPOCH.
+        let millis: JsValue = (1000 * self.inner.expires_at()).into();
+        Date::new(&millis)
+    }
+
+    #[wasm_bindgen(js_name = "notBefore")]
+    pub fn not_before(&self) -> Option {
+        // The UCAN value is the Unix Timestamp in seconds, but
+        // Date expects milliseconds since EPOCH.
+        self.inner.not_before().map(|time| {
+            let millis: JsValue = (1000 * time).into();
+            Date::new(&millis)
+        })
+    }
+
+    #[wasm_bindgen]
+    pub fn nonce(&self) -> Option {
+        self.inner.nonce().clone()
+    }
+
+    #[wasm_bindgen(js_name = "lifetimeBeginsBefore")]
+    pub fn lifetime_begins_before(&self, other: &WasmUcan) -> bool {
+        self.inner.lifetime_begins_before(&other.inner)
+    }
+
+    #[wasm_bindgen(js_name = "lifetimeEndsAfter")]
+    pub fn lifetime_ends_after(&self, other: &WasmUcan) -> bool {
+        self.inner.lifetime_ends_after(&other.inner)
+    }
+
+    #[wasm_bindgen(js_name = "lifetimeEncompasses")]
+    pub fn lifetime_encompasses(&self, other: &WasmUcan) -> bool {
+        self.inner.lifetime_encompasses(&other.inner)
+    }
+
+    #[wasm_bindgen]
+    pub fn attenuation(&self) -> Vec {
+        self.inner
+            .attenuation()
+            .into_iter()
+            .filter_map(|att| JsValue::from_serde(&att).ok())
+            .collect()
+    }
+
+    #[wasm_bindgen]
+    pub fn facts(&self) -> Vec {
+        self.inner
+            .facts()
+            .into_iter()
+            .filter_map(|fact| JsValue::from_serde(&fact).ok())
+            .collect()
+    }
+}
+
+#[wasm_bindgen]
+pub struct WasmSignable {
+    inner: Signable,
+}
+
+#[wasm_bindgen]
+impl WasmSignable {
+    pub fn sign(&self) -> WasmResult {
+        let signable = self.inner.clone();
+
+        Ok(future_to_promise(async move {
+            let inner = signable.sign().await.map_err(|err| WasmError::from(err))?;
+            let ucan = WasmUcan { inner };
+            Ok(ucan.into())
+        }))
+    }
+}
+
+#[wasm_bindgen]
+pub struct WasmUcanBuilder {
+    inner: UcanBuilder,
+}
+
+#[wasm_bindgen]
+impl WasmUcanBuilder {
+    #[wasm_bindgen(constructor)]
+    pub fn new() -> Self {
+        Self { inner: UcanBuilder::default() }
+    }
+
+    #[wasm_bindgen(js_name = "issuedBy")]
+    pub fn issued_by(self, issuer: &WebCryptoRsaKeyMaterial) -> Self {
+        Self {
+            inner: self.inner.issued_by(issuer),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "forAudience")]
+    pub fn for_audience(self, audience: &str) -> Self {
+        Self {
+            inner: self.inner.for_audience(audience),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "withLifetime")]
+    pub fn with_lifetime(self, seconds: u64) -> Self {
+        Self {
+            inner: self.inner.with_lifetime(seconds),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "withExpiration")]
+    pub fn with_expiration(self, timestamp: Date) -> Self {
+        // We need the timestamp in seconds.
+        let seconds = timestamp.get_time() as u64 / 1000;
+        Self {
+            inner: self.inner.with_expiration(seconds),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "notBefore")]
+    pub fn not_before(self, timestamp: Date) -> Self {
+        // We need the timestamp in seconds.
+        let seconds = timestamp.get_time() as u64 / 1000;
+        Self {
+            inner: self.inner.not_before(seconds),
+        }
+    }
+
+    #[wasm_bindgen]
+    pub fn with_nonce(self) -> Self {
+        Self {
+            inner: self.inner.with_nonce(),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "witnessedBy")]
+    pub fn witnessed_by(self, authority: &WasmUcan) -> Self {
+        Self {
+            inner: self.inner.witnessed_by(&authority.inner),
+        }
+    }
+
+    #[wasm_bindgen(js_name = "delegatingFrom")]
+    pub fn delegating_from(self, authority: &WasmUcan) -> Self {
+        Self {
+            inner: self.inner.delegating_from(&authority.inner),
+        }
+    }
+
+    #[wasm_bindgen]
+    pub fn build(self) -> WasmResult {
+        self.inner
+            .build()
+            .map(|inner| WasmSignable { inner })
+            .map_err(|err| WasmError::from(err))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use wasm_bindgen_test::*;
diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs
index d80f664d..bfb28530 100644
--- a/ucan/src/builder.rs
+++ b/ucan/src/builder.rs
@@ -20,11 +20,13 @@ use crate::ucan::Ucan;
 /// 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>
+
+#[derive(Clone)]
+pub struct Signable
 where
-    K: KeyMaterial,
+    K: KeyMaterial + Clone,
 {
-    pub issuer: &'a K,
+    pub issuer: K,
     pub audience: String,
 
     pub capabilities: Vec,
@@ -37,9 +39,9 @@ where
     pub add_nonce: bool,
 }
 
-impl<'a, K> Signable<'a, K>
+impl Signable
 where
-    K: KeyMaterial,
+    K: KeyMaterial + Clone,
 {
     pub const UCAN_VERSION: &'static str = "0.8.1";
 
@@ -99,11 +101,11 @@ where
 
 /// A builder API for UCAN tokens
 #[derive(Clone)]
-pub struct UcanBuilder<'a, K>
+pub struct UcanBuilder
 where
     K: KeyMaterial,
 {
-    issuer: Option<&'a K>,
+    issuer: Option,
     audience: Option,
 
     capabilities: Vec,
@@ -117,7 +119,7 @@ where
     add_nonce: bool,
 }
 
-impl<'a, K> Default for UcanBuilder<'a, K>
+impl Default for UcanBuilder
 where
     K: KeyMaterial,
 {
@@ -147,13 +149,13 @@ where
     }
 }
 
-impl<'a, K> UcanBuilder<'a, K>
+impl UcanBuilder
 where
-    K: KeyMaterial,
+    K: KeyMaterial + Clone,
 {
     /// 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);
+    pub fn issued_by(mut self, issuer: &K) -> Self {
+        self.issuer = Some(issuer.clone());
         self
     }
 
@@ -269,12 +271,12 @@ where
         }
     }
 
-    pub fn build(self) -> Result> {
+    pub fn build(self) -> Result> {
         match &self.issuer {
             Some(issuer) => match &self.audience {
                 Some(audience) => match self.implied_expiration() {
                     Some(expiration) => Ok(Signable {
-                        issuer,
+                        issuer: issuer.clone(),
                         audience: audience.clone(),
                         not_before: self.not_before,
                         expiration,