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,