Packages
macula
3.11.1
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
Current section
Files
native/macula_did_nif/src/lib.rs
//! Macula DID NIF operations.
//!
//! This module provides Decentralized Identifier (DID) operations for the Macula mesh:
//!
//! - DID Document creation with Ed25519 verification methods
//! - DID resolution (local cache lookup)
//! - Controller verification
//! - DID-to-public-key extraction
//!
//! DID Format: `did:macula:<identity>`
//! - Realm: `did:macula:io.macula`
//! - Org: `did:macula:io.macula.{org}`
//! - App: `did:macula:io.macula.{org}.{app}`
use ed25519_dalek::VerifyingKey;
use rustler::{Atom, Binary, Env, NifResult, OwnedBinary};
use serde::{Deserialize, Serialize};
mod atoms {
rustler::atoms! {
ok,
error,
invalid_did,
invalid_public_key,
invalid_document,
not_found,
controller_mismatch,
malformed_json,
}
}
/// DID Document verification method
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerificationMethod {
id: String,
#[serde(rename = "type")]
method_type: String,
controller: String,
public_key_multibase: String,
}
/// DID Document (W3C DID Core compatible)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DidDocument {
#[serde(rename = "@context")]
context: Vec<String>,
id: String,
controller: Option<String>,
verification_method: Vec<VerificationMethod>,
authentication: Vec<String>,
assertion_method: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
capability_invocation: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
capability_delegation: Option<Vec<String>>,
}
/// Create a new DID Document.
///
/// Arguments:
/// - did: The DID string (e.g., "did:macula:io.macula.acme")
/// - public_key: 32-byte Ed25519 public key
///
/// Returns:
/// - `{ok, DocumentJson}` - JSON-encoded DID document
/// - `{error, Reason}` on failure
#[rustler::nif]
fn nif_create_document<'a>(
env: Env<'a>,
did: Binary,
public_key: Binary,
) -> NifResult<(Atom, Binary<'a>)> {
let did_str = match std::str::from_utf8(did.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_did()),
};
// Validate DID format
if !did_str.starts_with("did:macula:") {
return result_error(env, atoms::invalid_did());
}
// Validate public key
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()),
};
// Validate it's a valid Ed25519 public key
if VerifyingKey::from_bytes(&pk_bytes).is_err() {
return result_error(env, atoms::invalid_public_key());
}
// Encode public key as multibase (base58btc with 'z' prefix)
// For Ed25519 public keys, we use the multicodec prefix 0xed01
let mut multicodec_key = vec![0xed, 0x01];
multicodec_key.extend_from_slice(&pk_bytes);
let multibase_key = format!("z{}", bs58::encode(&multicodec_key).into_string());
// Create key ID
let key_id = format!("{}#key-1", did_str);
// Determine controller (parent in hierarchy)
let controller = get_parent_did(did_str);
// Build DID Document
let doc = DidDocument {
context: vec!["https://www.w3.org/ns/did/v1".to_string()],
id: did_str.to_string(),
controller,
verification_method: vec![VerificationMethod {
id: key_id.clone(),
method_type: "Ed25519VerificationKey2020".to_string(),
controller: did_str.to_string(),
public_key_multibase: multibase_key,
}],
authentication: vec![key_id.clone()],
assertion_method: vec![key_id.clone()],
capability_invocation: Some(vec![key_id.clone()]),
capability_delegation: Some(vec![key_id]),
};
// Serialize to JSON
let json = match serde_json::to_string(&doc) {
Ok(j) => j,
Err(_) => return result_error(env, atoms::malformed_json()),
};
let mut output = OwnedBinary::new(json.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(json.as_bytes());
Ok((atoms::ok(), output.release(env)))
}
/// Parse a DID Document from JSON.
///
/// Arguments:
/// - document_json: JSON-encoded DID document
///
/// Returns:
/// - `{ok, DocumentJson}` - validated and re-serialized document
/// - `{error, invalid_document}` if parsing fails
#[rustler::nif]
fn nif_parse_document<'a>(env: Env<'a>, document_json: Binary) -> NifResult<(Atom, Binary<'a>)> {
let json_str = match std::str::from_utf8(document_json.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let _doc: DidDocument = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return result_error(env, atoms::invalid_document()),
};
// Return the original JSON (validated)
let mut output = OwnedBinary::new(document_json.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output
.as_mut_slice()
.copy_from_slice(document_json.as_slice());
Ok((atoms::ok(), output.release(env)))
}
/// Extract the public key from a DID Document.
///
/// Arguments:
/// - document_json: JSON-encoded DID document
///
/// Returns:
/// - `{ok, PublicKey}` - 32-byte Ed25519 public key
/// - `{error, Reason}` on failure
#[rustler::nif]
fn nif_extract_public_key<'a>(
env: Env<'a>,
document_json: Binary,
) -> NifResult<(Atom, Binary<'a>)> {
let json_str = match std::str::from_utf8(document_json.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let doc: DidDocument = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return result_error(env, atoms::invalid_document()),
};
// Get the first verification method
let vm = match doc.verification_method.first() {
Some(v) => v,
None => return result_error(env, atoms::invalid_document()),
};
// Decode multibase public key
let multibase_key = &vm.public_key_multibase;
if !multibase_key.starts_with('z') {
return result_error(env, atoms::invalid_public_key());
}
let decoded = match bs58::decode(&multibase_key[1..]).into_vec() {
Ok(d) => d,
Err(_) => return result_error(env, atoms::invalid_public_key()),
};
// Check multicodec prefix (0xed01 for Ed25519)
if decoded.len() < 34 || decoded[0] != 0xed || decoded[1] != 0x01 {
return result_error(env, atoms::invalid_public_key());
}
let pk_bytes = &decoded[2..34];
let mut output = OwnedBinary::new(32).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(pk_bytes);
Ok((atoms::ok(), output.release(env)))
}
/// Get the DID from a DID Document.
///
/// Arguments:
/// - document_json: JSON-encoded DID document
///
/// Returns:
/// - `{ok, Did}` - DID string
/// - `{error, invalid_document}` if parsing fails
#[rustler::nif]
fn nif_get_did<'a>(env: Env<'a>, document_json: Binary) -> NifResult<(Atom, Binary<'a>)> {
let json_str = match std::str::from_utf8(document_json.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let doc: DidDocument = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let did = doc.id.as_bytes();
let mut output = OwnedBinary::new(did.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(did);
Ok((atoms::ok(), output.release(env)))
}
/// Get the controller DID from a DID Document.
///
/// Arguments:
/// - document_json: JSON-encoded DID document
///
/// Returns:
/// - `{ok, ControllerDid}` - Controller DID string (or self if no controller)
/// - `{error, invalid_document}` if parsing fails
#[rustler::nif]
fn nif_get_controller<'a>(env: Env<'a>, document_json: Binary) -> NifResult<(Atom, Binary<'a>)> {
let json_str = match std::str::from_utf8(document_json.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let doc: DidDocument = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return result_error(env, atoms::invalid_document()),
};
let controller = doc.controller.unwrap_or(doc.id);
let controller_bytes = controller.as_bytes();
let mut output = OwnedBinary::new(controller_bytes.len()).ok_or(rustler::Error::Term(
Box::new("Failed to allocate binary"),
))?;
output.as_mut_slice().copy_from_slice(controller_bytes);
Ok((atoms::ok(), output.release(env)))
}
/// Verify that a DID Document is controlled by the expected controller.
///
/// Arguments:
/// - document_json: JSON-encoded DID document
/// - expected_controller: The expected controller DID
///
/// Returns:
/// - `ok` if controller matches
/// - `{error, controller_mismatch}` if controller doesn't match
/// - `{error, invalid_document}` if parsing fails
#[rustler::nif]
fn nif_verify_controller(document_json: Binary, expected_controller: Binary) -> Atom {
let json_str = match std::str::from_utf8(document_json.as_slice()) {
Ok(s) => s,
Err(_) => return atoms::invalid_document(),
};
let expected = match std::str::from_utf8(expected_controller.as_slice()) {
Ok(s) => s,
Err(_) => return atoms::invalid_did(),
};
let doc: DidDocument = match serde_json::from_str(json_str) {
Ok(d) => d,
Err(_) => return atoms::invalid_document(),
};
let actual_controller = doc.controller.as_ref().unwrap_or(&doc.id);
if actual_controller == expected {
atoms::ok()
} else {
atoms::controller_mismatch()
}
}
/// Parse a DID string and extract its components.
///
/// Arguments:
/// - did: The DID string (e.g., "did:macula:io.macula.acme.myapp")
///
/// Returns:
/// - `{ok, ComponentsJson}` - JSON object with method, identity, and parts
/// - `{error, invalid_did}` if parsing fails
#[rustler::nif]
fn nif_parse_did<'a>(env: Env<'a>, did: Binary) -> NifResult<(Atom, Binary<'a>)> {
let did_str = match std::str::from_utf8(did.as_slice()) {
Ok(s) => s,
Err(_) => return result_error(env, atoms::invalid_did()),
};
// Parse DID format: did:method:identity
let parts: Vec<&str> = did_str.split(':').collect();
if parts.len() != 3 || parts[0] != "did" || parts[1] != "macula" {
return result_error(env, atoms::invalid_did());
}
let identity = parts[2];
let identity_parts: Vec<&str> = identity.split('.').collect();
let result = serde_json::json!({
"method": "macula",
"identity": identity,
"parts": identity_parts,
"depth": identity_parts.len()
});
let json = result.to_string();
let mut output = OwnedBinary::new(json.len()).ok_or(rustler::Error::Term(Box::new(
"Failed to allocate binary",
)))?;
output.as_mut_slice().copy_from_slice(json.as_bytes());
Ok((atoms::ok(), output.release(env)))
}
/// Check if one DID is a descendant of another.
///
/// Arguments:
/// - child_did: The potential child DID
/// - parent_did: The potential parent DID
///
/// Returns:
/// - `true` if child is a descendant of parent
/// - `false` otherwise
#[rustler::nif]
fn nif_is_descendant(child_did: Binary, parent_did: Binary) -> bool {
let child = match std::str::from_utf8(child_did.as_slice()) {
Ok(s) => s,
Err(_) => return false,
};
let parent = match std::str::from_utf8(parent_did.as_slice()) {
Ok(s) => s,
Err(_) => return false,
};
// Extract identities from DIDs
let child_identity = match child.strip_prefix("did:macula:") {
Some(i) => i,
None => return false,
};
let parent_identity = match parent.strip_prefix("did:macula:") {
Some(i) => i,
None => return false,
};
// Child must start with parent identity + "."
if child_identity == parent_identity {
return false; // Same DID, not descendant
}
child_identity.starts_with(&format!("{}.", parent_identity))
}
// Helper: Get parent DID from a DID
fn get_parent_did(did: &str) -> Option<String> {
let identity = did.strip_prefix("did:macula:")?;
let parts: Vec<&str> = identity.split('.').collect();
if parts.len() <= 2 {
// Root realm (io.macula) has no parent
None
} else {
// Remove last part to get parent
let parent_parts = &parts[..parts.len() - 1];
Some(format!("did:macula:{}", parent_parts.join(".")))
}
}
// 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_did_nif");