Packages

macula

4.4.6
7.1.0 7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula native macula_ucan_nif src lib.rs
Raw

native/macula_ucan_nif/src/lib.rs

//! Macula UCAN NIF operations.
//!
//! This module provides high-performance UCAN (User Controlled Authorization Networks)
//! token operations for the Macula mesh:
//!
//! - Token creation with Ed25519 signing
//! - Token verification (signature and expiration)
//! - JWT-like encoding/decoding (header.payload.signature)
//! - Token CID (Content ID) generation for proof chains
//!
//! UCAN tokens are self-contained, delegatable capability tokens that enable
//! decentralized authorization without requiring a central authority.
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, SigningKey, Signer, VerifyingKey, Verifier};
use rustler::{Atom, Binary, Env, NifResult, OwnedBinary};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
mod atoms {
rustler::atoms! {
ok,
error,
invalid_token,
invalid_signature,
invalid_private_key,
invalid_public_key,
expired,
not_yet_valid,
malformed_json,
}
}
/// UCAN Header
#[derive(Debug, Serialize, Deserialize)]
struct UcanHeader {
alg: String, // "EdDSA"
typ: String, // "JWT"
ucv: String, // UCAN version "0.10.0"
}
/// UCAN Payload
#[derive(Debug, Serialize, Deserialize)]
struct UcanPayload {
iss: String, // Issuer DID
aud: String, // Audience DID
#[serde(skip_serializing_if = "Option::is_none")]
exp: Option<u64>, // Expiration (unix timestamp)
#[serde(skip_serializing_if = "Option::is_none")]
nbf: Option<u64>, // Not before (unix timestamp)
#[serde(skip_serializing_if = "Option::is_none")]
nnc: Option<String>, // Nonce (for uniqueness)
cap: serde_json::Value, // Capabilities (array of {with, can})
#[serde(skip_serializing_if = "Option::is_none")]
fct: Option<serde_json::Value>, // Facts (metadata)
prf: Vec<String>, // Proof chain (CIDs of parent UCANs)
}
/// Create a new UCAN token.
///
/// Arguments:
/// - issuer: Issuer DID (binary string)
/// - audience: Audience DID (binary string)
/// - capabilities_json: JSON array of capabilities [{with, can}, ...]
/// - private_key: 32-byte Ed25519 private key
/// - opts_json: JSON object with optional fields (exp, nbf, nnc, fct, prf)
///
/// Returns:
/// - `{ok, Token}` where Token is the encoded JWT string
/// - `{error, Reason}` on failure
#[rustler::nif]
fn nif_create<'a>(
env: Env<'a>,
issuer: Binary,
audience: Binary,
capabilities_json: Binary,
private_key: Binary,
opts_json: Binary,
) -> NifResult<(Atom, Binary<'a>)> {
// Parse capabilities
let capabilities: serde_json::Value = match serde_json::from_slice(capabilities_json.as_slice()) {
Ok(v) => v,
Err(_) => return result_error(env, atoms::malformed_json()),
};
// Parse options
let opts: serde_json::Value = match serde_json::from_slice(opts_json.as_slice()) {
Ok(v) => v,
Err(_) => serde_json::json!({}),
};
// Validate private key
if private_key.len() != 32 {
return result_error(env, atoms::invalid_private_key());
}
let key_bytes: [u8; 32] = match private_key.as_slice().try_into() {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_private_key()),
};
let signing_key = SigningKey::from_bytes(&key_bytes);
// Build header
let header = UcanHeader {
alg: "EdDSA".to_string(),
typ: "JWT".to_string(),
ucv: "0.10.0".to_string(),
};
// Build payload
let payload = UcanPayload {
iss: String::from_utf8_lossy(issuer.as_slice()).to_string(),
aud: String::from_utf8_lossy(audience.as_slice()).to_string(),
exp: opts.get("exp").and_then(|v| v.as_u64()),
nbf: opts.get("nbf").and_then(|v| v.as_u64()),
nnc: opts.get("nnc").and_then(|v| v.as_str().map(String::from)),
cap: capabilities,
fct: opts.get("fct").cloned(),
prf: opts.get("prf")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default(),
};
// Encode header and payload
let header_json = match serde_json::to_string(&header) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::malformed_json()),
};
let payload_json = match serde_json::to_string(&payload) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::malformed_json()),
};
let header_b64 = URL_SAFE_NO_PAD.encode(header_json.as_bytes());
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
// Sign header.payload
let signing_input = format!("{}.{}", header_b64, payload_b64);
let signature = signing_key.sign(signing_input.as_bytes());
let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
// Combine into JWT format
let token = format!("{}.{}.{}", header_b64, payload_b64, signature_b64);
// Return token
let mut output = OwnedBinary::new(token.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(token.as_bytes());
Ok((atoms::ok(), output.release(env)))
}
/// Verify a UCAN token.
///
/// Arguments:
/// - token: The encoded JWT token (binary string)
/// - public_key: 32-byte Ed25519 public key of the issuer
///
/// Returns:
/// - `{ok, PayloadJson}` if valid, with the decoded payload
/// - `{error, invalid_signature}` if signature doesn't match
/// - `{error, expired}` if token is expired
/// - `{error, not_yet_valid}` if token is not yet valid
/// - `{error, invalid_token}` if token is malformed
#[rustler::nif]
fn nif_verify<'a>(env: Env<'a>, token: Binary, public_key: Binary) -> NifResult<(Atom, Binary<'a>)> {
let token_str = match std::str::from_utf8(token.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_token()),
};
// Split token into parts
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 3 {
return result_error(env, atoms::invalid_token());
}
let header_b64 = parts[0];
let payload_b64 = parts[1];
let signature_b64 = parts[2];
// Decode and parse payload
let payload_bytes = match URL_SAFE_NO_PAD.decode(payload_b64) {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let payload: UcanPayload = match serde_json::from_slice(&payload_bytes) {
Ok(p) => p,
Err(_) => return result_error(env, atoms::invalid_token()),
};
// Check expiration
if let Some(exp) = payload.exp {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now > exp {
return result_error(env, atoms::expired());
}
}
// Check not-before
if let Some(nbf) = payload.nbf {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now < nbf {
return result_error(env, atoms::not_yet_valid());
}
}
// Verify signature
if public_key.len() != 32 {
return result_error(env, atoms::invalid_public_key());
}
let pk_bytes: [u8; 32] = match public_key.as_slice().try_into() {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_public_key()),
};
let verifying_key = match VerifyingKey::from_bytes(&pk_bytes) {
Ok(k) => k,
Err(_) => return result_error(env, atoms::invalid_public_key()),
};
let sig_bytes = match URL_SAFE_NO_PAD.decode(signature_b64) {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_signature()),
};
if sig_bytes.len() != 64 {
return result_error(env, atoms::invalid_signature());
}
let sig_array: [u8; 64] = match sig_bytes.try_into() {
Ok(a) => a,
Err(_) => return result_error(env, atoms::invalid_signature()),
};
let signature = Signature::from_bytes(&sig_array);
let signing_input = format!("{}.{}", header_b64, payload_b64);
if verifying_key.verify(signing_input.as_bytes(), &signature).is_err() {
return result_error(env, atoms::invalid_signature());
}
// Return payload as JSON
let payload_json = match serde_json::to_string(&payload) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let mut output = OwnedBinary::new(payload_json.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(payload_json.as_bytes());
Ok((atoms::ok(), output.release(env)))
}
/// Decode a UCAN token without verification.
///
/// Arguments:
/// - token: The encoded JWT token (binary string)
///
/// Returns:
/// - `{ok, PayloadJson}` with the decoded payload (unverified!)
/// - `{error, invalid_token}` if token is malformed
#[rustler::nif]
fn nif_decode<'a>(env: Env<'a>, token: Binary) -> NifResult<(Atom, Binary<'a>)> {
let token_str = match std::str::from_utf8(token.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 3 {
return result_error(env, atoms::invalid_token());
}
let payload_bytes = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_token()),
};
// Validate it's valid JSON
let _: serde_json::Value = match serde_json::from_slice(&payload_bytes) {
Ok(v) => v,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let mut output = OwnedBinary::new(payload_bytes.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(&payload_bytes);
Ok((atoms::ok(), output.release(env)))
}
/// Compute the CID (Content ID) of a UCAN token.
///
/// The CID is a SHA-256 hash of the token, encoded as URL-safe base64.
/// Used for proof chains.
///
/// Arguments:
/// - token: The encoded JWT token (binary string)
///
/// Returns:
/// - CID as URL-safe base64 binary string
#[rustler::nif]
fn nif_compute_cid<'a>(env: Env<'a>, token: Binary) -> NifResult<Binary<'a>> {
let mut hasher = Sha256::new();
hasher.update(token.as_slice());
let hash = hasher.finalize();
let cid = URL_SAFE_NO_PAD.encode(hash);
let mut output = OwnedBinary::new(cid.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(cid.as_bytes());
Ok(output.release(env))
}
/// Extract the issuer DID from a UCAN token.
///
/// Arguments:
/// - token: The encoded JWT token (binary string)
///
/// Returns:
/// - `{ok, IssuerDid}` binary string
/// - `{error, invalid_token}` if malformed
#[rustler::nif]
fn nif_get_issuer<'a>(env: Env<'a>, token: Binary) -> NifResult<(Atom, Binary<'a>)> {
let token_str = match std::str::from_utf8(token.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 3 {
return result_error(env, atoms::invalid_token());
}
let payload_bytes = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let payload: UcanPayload = match serde_json::from_slice(&payload_bytes) {
Ok(p) => p,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let iss_bytes = payload.iss.as_bytes();
let mut output = OwnedBinary::new(iss_bytes.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(iss_bytes);
Ok((atoms::ok(), output.release(env)))
}
/// Extract the audience DID from a UCAN token.
///
/// Arguments:
/// - token: The encoded JWT token (binary string)
///
/// Returns:
/// - `{ok, AudienceDid}` binary string
/// - `{error, invalid_token}` if malformed
#[rustler::nif]
fn nif_get_audience<'a>(env: Env<'a>, token: Binary) -> NifResult<(Atom, Binary<'a>)> {
let token_str = match std::str::from_utf8(token.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let parts: Vec<&str> = token_str.split('.').collect();
if parts.len() != 3 {
return result_error(env, atoms::invalid_token());
}
let payload_bytes = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(b) => b,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let payload: UcanPayload = match serde_json::from_slice(&payload_bytes) {
Ok(p) => p,
Err(_) => return result_error(env, atoms::invalid_token()),
};
let aud_bytes = payload.aud.as_bytes();
let mut output = OwnedBinary::new(aud_bytes.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(aud_bytes);
Ok((atoms::ok(), output.release(env)))
}
// Helper to return error tuple with empty binary
fn result_error<'a>(env: Env<'a>, reason: Atom) -> NifResult<(Atom, Binary<'a>)> {
let empty = OwnedBinary::new(0).ok_or(rustler::Error::Term(Box::new("alloc")))?;
Ok((reason, empty.release(env)))
}
rustler::init!("macula_ucan_nif");