Macula Authorization Guide

View Source

This guide provides a comprehensive overview of Macula's authorization system, including decentralized identity, capability-based permissions, self-sovereign certificates, and UCAN-based licensing.

Overview

Macula implements decentralized authorization using industry-standard cryptographic primitives. Unlike traditional client-server authorization where a central authority validates requests, Macula's authorization is:

  • Self-sovereign: Identity controlled by the owner's keypair
  • Cryptographically verifiable: No network calls needed for validation
  • Capability-based: Fine-grained permissions via UCAN tokens
  • Offline-capable: All validation happens locally

Authorization Flow


Core Concepts

Decentralized Identifiers (DIDs)

A DID (Decentralized Identifier) is a globally unique identifier that enables verifiable, decentralized digital identity. DIDs are defined by the W3C DID Core Specification.

DID Structure

Macula DID Format

did:macula:io.macula.rgfaber
|   |      +------------------ Method-specific identifier (namespace)
|   +-------------------------- Method (macula)
+------------------------------- Scheme (always "did")

Key Properties:

PropertyDescription
Self-sovereignControlled by owner's Ed25519 keypair
Human-readableHierarchical namespace format
ResolvableMethod defines how to obtain public key
Cryptographically verifiableOwnership proven via signature

DID Resolution

When a DID is encountered, Macula resolves it to extract:

  1. Method: macula (defines resolution rules)
  2. Identity: io.macula.rgfaber (namespace path)
  3. Parts: ["io", "macula", "rgfaber"] (hierarchy)
  4. Depth: 3 (hierarchy level)

The macula_did_cache module provides high-performance caching using Erlang's persistent_term:

%% Parse and cache a DID (or retrieve from cache)
{ok, Parsed} = macula_did_cache:get_or_parse(<<"did:macula:io.macula.rgfaber">>).
%% => {ok, #{<<"method">> => <<"macula">>,
%%           <<"identity">> => <<"io.macula.rgfaber">>,
%%           <<"parts">> => [<<"io">>, <<"macula">>, <<"rgfaber">>],
%%           <<"depth">> => 3}}

Performance: persistent_term provides O(1) lookups with zero garbage collection impact, making it ideal for frequently-accessed identity data.

DID to Common Name Conversion

%% DID to CN (reverse the parts)
CN = macula_cert:did_to_cn(<<"did:macula:io.example.org.app.node01">>).
%% Result: <<"app.node01.io.example.org">>

%% CN to DID
DID = macula_cert:cn_to_did(<<"app.node01.io.example.org">>).
%% Result: <<"did:macula:io.example.org.app.node01">>

Further Reading:


User Controlled Authorization Networks (UCANs)

UCAN (User Controlled Authorization Networks) is a capability-based authorization system built on JWT (JSON Web Tokens). UCANs enable delegation chains where permissions can be granted and re-delegated without involving a central authority.

UCAN Token Structure

UCAN Token Structure

A UCAN is a JWT with specific claims:

ClaimDescription
issIssuer DID - Who created and signed this token
audAudience DID - Who this token is granted to
expExpiration - Unix timestamp when token expires
nbfNot Before - Token valid only after this time
capCapabilities - Array of permission grants
prfProofs - Chain of parent UCANs (for delegation)

Capability Format

Capabilities define what actions are permitted on which resources:

#{
    <<"with">> => <<"io.macula.rgfaber.*">>,   % Resource pattern
    <<"can">> => <<"mesh:call">>               % Operation
}

Supported Operations:

OperationDescription
mesh:callRPC procedure invocation
mesh:publishPublish to topic
mesh:subscribeSubscribe to topic
mesh:*All mesh operations

Wildcards:

  • io.macula.rgfaber.* matches any resource in namespace
  • mesh:* matches any mesh operation

Delegation Chains

UCANs support attenuated delegation: permissions can be re-delegated but only with equal or narrower scope.

Alice (owns io.macula.alice.*)
  |
  +---> grants Bob: mesh:call on io.macula.alice.api.*
        |
        +---> Bob grants Carol: mesh:call on io.macula.alice.api.read_only
              (valid - narrower scope)

        +---> Bob grants Dave: mesh:call on io.macula.alice.*
              (INVALID - broader than Bob received)

Further Reading:


Namespace Ownership Model

Macula uses a hierarchical namespace ownership model where DIDs map directly to namespaces they control:

Namespace Hierarchy

DID to Namespace Mapping

DID                              Owns Namespace
-------------------------------  -----------------------------
did:macula:io.macula             io.macula.* (realm root)
did:macula:io.macula.rgfaber     io.macula.rgfaber.*
did:macula:io.macula.ibm         io.macula.ibm.*
did:macula:io.macula.ibm.watson  io.macula.ibm.watson.*

Hierarchical Access Rules

  1. Owner access: A DID can always access its own namespace
  2. Parent access: Parent DIDs can access child namespaces
  3. Sibling isolation: Siblings cannot access each other's namespaces
%% Examples
did:macula:io.macula.ibm         -> CAN access io.macula.ibm.watson.*
did:macula:io.macula.ibm.watson  -> CANNOT access io.macula.ibm.*
did:macula:io.macula.rgfaber     -> CANNOT access io.macula.ibm.*

Self-Sovereign Certificates

Macula uses Ed25519-based certificates anchored to DIDs, enabling identity without external certificate authorities.

Certificate Hierarchy

Realm Certificate (self-signed)
did:macula:io.customer.org
     |
     +-- Instance Certificate (signed by realm)
     |   did:macula:io.customer.org.app1.node01
     |
     +-- Instance Certificate (signed by realm)
     |   did:macula:io.customer.org.app1.node02
     |
     +-- Instance Certificate (signed by realm)
         did:macula:io.customer.org.app2.node01

Certificate Structure

#{
    subject => #{
        did => <<"did:macula:io.customer.org.app.node01">>,
        common_name => <<"app.node01.io.customer.org">>
    },
    issuer => #{
        did => <<"did:macula:io.customer.org">>,
        common_name => <<"io.customer.org">>
    },
    not_before => 1704067200,
    not_after => 1735689600,
    public_key => <<...>>,   %% Ed25519 (32 bytes)
    signature => <<...>>     %% Ed25519 (64 bytes)
}

Certificate API

Generate Keypair

{PubKey, PrivKey} = macula_cert:generate_keypair().
%% PubKey: 32 bytes, PrivKey: 64 bytes (seed + public key)

Create Realm Certificate (Self-Signed)

RealmDID = <<"did:macula:io.example.org">>,
{PubKey, PrivKey} = macula_cert:generate_keypair(),

{ok, RealmCert} = macula_cert:generate_realm_cert(RealmDID, PubKey, PrivKey).

%% Or with custom validity
{ok, RealmCert} = macula_cert:generate_realm_cert(RealmDID, PubKey, PrivKey, 365).

Create Instance Certificate (Signed by Realm)

InstanceDID = <<"did:macula:io.example.org.app.node01">>,
{InstancePubKey, _} = macula_cert:generate_keypair(),

{ok, InstanceCert} = macula_cert:generate_instance_cert(
    InstanceDID, InstancePubKey, RealmCert, RealmPrivKey
).

%% Or with custom validity (90 days)
{ok, InstanceCert} = macula_cert:generate_instance_cert(
    InstanceDID, InstancePubKey, RealmCert, RealmPrivKey, 90
).

Verify Certificates

ok = macula_cert:verify_self_signed(RealmCert).
ok = macula_cert:verify_cert(InstanceCert, RealmCert).
true = macula_cert:is_valid_now(InstanceCert).

Encode/Decode

{ok, Binary} = macula_cert:encode(Cert).
{ok, Cert} = macula_cert:decode(Binary).
Map = macula_cert:to_map(Cert).
{ok, Cert} = macula_cert:from_map(Map).

Trust Store

The trust store manages trusted realm certificates.

{ok, _Pid} = macula_trust_store:start_link().

%% Add a trusted realm
ok = macula_trust_store:add_trusted_realm(RealmDID, RealmCert).

%% Verify instance cert chains back to a trusted realm
ok = macula_trust_store:verify_instance_cert(InstanceCert).

%% Query trust store
true = macula_trust_store:is_trusted(RealmDID).
{ok, RealmCert} = macula_trust_store:get_realm_cert(RealmDID).
TrustedRealms = macula_trust_store:list_trusted().

Certificate Security Considerations

  • Private keys should never leave the generating node
  • Instance certificates should have shorter validity (30-90 days)
  • Realm certificates can have longer validity (1-5 years)
  • Implement automatic renewal before expiration
AspectSelf-SovereignTraditional PKI
Trust rootRealm certificateExternal CA
IssuanceInstant, localRequires CA interaction
CostFreeOften paid
PrivacyNo third partyCA sees all certs
RevocationLimitedCRL/OCSP
InteropMacula ecosystemWeb browsers, etc.

UCAN-Based Licensing

Macula uses UCAN tokens for software licensing in the marketplace, providing decentralized, capability-based license management.

License Models

ModelDescriptionUCAN Configuration
FreeNo restrictionsAuto-granted, no expiration
TrialTime-limited evaluationShort exp (14-30 days)
PerpetualOne-time purchaseNo expiration
SubscriptionRecurring paymentexp set to subscription end
Per-seatLimited installationsnb.max_nodes claim

License Token Structure

{
  "iss": "did:macula:io.publisher.org",
  "aud": "did:macula:io.customer.org",
  "exp": 1735689600,
  "nbf": 1704067200,
  "att": [
    {
      "with": "did:macula:io.publisher.org/app/my_app",
      "can": "artifact/run"
    },
    {
      "with": "did:macula:io.publisher.org/app/my_app",
      "can": "artifact/update"
    }
  ],
  "fct": {
    "license_model": "subscription",
    "max_nodes": 5
  }
}

License Capability Types

CapabilityDescription
artifact/runPermission to run the application
artifact/updatePermission to update to new versions
artifact/installPermission to install the application

License Validation

case bc_gitops_license:validate(LicenseSpec, #{app_name => Name}) of
    ok -> deploy(AppSpec);
    {warning, expiring_soon} -> log_warning("License expiring soon"), deploy(AppSpec);
    {error, expired} -> {error, {license_validation_failed, expired}};
    {error, invalid_signature} -> {error, {license_validation_failed, invalid_signature}};
    {error, node_limit_exceeded} -> {error, {license_validation_failed, node_limit_exceeded}}
end.

License Procurement Flows

Free Applications: User installs, UCAN auto-granted with no expiration.

Trial Applications: User starts trial, UCAN generated with 14-day expiration.

Paid Applications: User installs, payment required, publisher issues UCAN after payment, user retries install.

License Renewal

  1. User/system requests renewal from publisher
  2. Payment processed (if subscription)
  3. Publisher issues new UCAN with extended expiration
  4. Update app.config with new UCAN
  5. bc-gitops reconciles with new license

Authorization Flow

When a mesh operation (RPC call, publish, subscribe) is requested, Macula follows this authorization flow:

Step 1: Extract Caller DID

The caller's DID is extracted from:

  • TLS Certificate SAN (Subject Alternative Name URI field)
  • Message Header (for explicit caller identification)

Step 2: Extract Namespace

The namespace is derived from the topic/procedure:

io.macula.rgfaber.place_order -> io.macula.rgfaber

Step 3: Check Ownership

case macula_authorization:check_namespace_ownership(CallerDID, Namespace) of
    {ok, owner} -> authorized;
    {ok, ancestor} -> authorized;
    {error, not_owner} -> check_ucan()
end.

Step 4: Validate UCAN (if needed)

If the caller doesn't own the namespace, a valid UCAN token is required:

case macula_authorization:validate_ucan_for_operation(UcanToken, CallerDID, Resource, Operation) of
    {ok, authorized} -> proceed;
    {error, Reason} -> deny
end.

Public Topics

Topics containing .public. are world-readable without ownership or UCAN:

io.macula.rgfaber.public.announcements  -> Anyone can subscribe
io.macula.public.news                   -> Anyone can subscribe

Note: Publishing to public topics still requires ownership or a valid UCAN grant.


Revocation

UCANs can be revoked before expiration using the revocation system:

Revocation Flow

Revocation Process

  1. Issuer initiates: Only the token issuer can revoke
  2. Compute CID: SHA-256 hash of the UCAN, base64url encoded
  3. Sign revocation: Ed25519 signature proves issuer authority
  4. Broadcast: Published to io.macula.system.ucan_revoked topic
  5. Cache locally: Each node stores in ETS with TTL

Safeguards

SafeguardImplementation
Issuer-onlySignature validated against issuer's public key
Rate limitingMax 10 revocations per issuer per minute
Auto-expiryRevocation cached until original UCAN exp time
Ed25519 required64-byte signature format enforced

Revocation API

{ok, CID} = macula_ucan_revocation:revoke(IssuerDID, UcanToken, ExpiresAt).
true = macula_ucan_revocation:is_revoked(IssuerDID, CID).

Audit Logging

All authorization decisions are logged via the audit system:

Audit System

Telemetry Events

EventDescription
[macula, authorization, allowed]Authorization succeeded
[macula, authorization, denied]Authorization denied
[macula, authorization, error]Authorization check failed

Event Metadata

#{
    operation => call | publish | subscribe | announce,
    caller => <<"did:macula:io.macula.rgfaber">>,
    resource => <<"io.macula.other.service">>,
    timestamp => 1704672000,
    reason => unauthorized | invalid_ucan | expired_ucan | ...
}

ETS Storage (Optional)

macula_authorization_audit:enable().
macula_authorization_audit:disable().
Entries = macula_authorization_audit:get_recent(100).
CallerEntries = macula_authorization_audit:get_by_caller(CallerDID, 50).
Stats = macula_authorization_audit:get_stats().

LRU Eviction

LRU Eviction

When the log exceeds max_entries, the oldest entries (by timestamp) are evicted, ensuring bounded memory usage while retaining the most recent authorization decisions.

macula_authorization_audit:set_max_entries(5000).
macula_authorization_audit:set_retention(7200).

API Reference

Core Authorization

macula_authorization:check_rpc_call(CallerDID, Procedure, UcanToken, Opts).
macula_authorization:check_publish(CallerDID, Topic, UcanToken, Opts).
macula_authorization:check_subscribe(CallerDID, Topic, Opts).
macula_authorization:check_subscribe(CallerDID, Topic, UcanToken, Opts).
macula_authorization:check_announce(CallerDID, Procedure, Opts).

Namespace Operations

macula_authorization:extract_namespace(<<"io.macula.rgfaber.service">>).
macula_authorization:check_namespace_ownership(CallerDID, Namespace).
macula_authorization:is_public_topic(<<"io.macula.rgfaber.public.news">>).

DID Cache

macula_did_cache:get_or_parse(DID).
macula_did_cache:invalidate(DID).
macula_did_cache:clear().
macula_did_cache:cache_size().

UCAN Revocation

{ok, Pid} = macula_ucan_revocation:start_link().
{ok, CID} = macula_ucan_revocation:revoke(IssuerDID, UcanToken, ExpiresAt).
macula_ucan_revocation:is_revoked(IssuerDID, CID).
macula_ucan_revocation:get_stats().

Audit Logging

{ok, Pid} = macula_authorization_audit:start_link().
macula_authorization_audit:log_authorized(Operation, CallerDID, Resource).
macula_authorization_audit:log_denied(Operation, CallerDID, Resource, Reason).
macula_authorization_audit:get_recent(Limit).
macula_authorization_audit:get_by_caller(CallerDID, Limit).
macula_authorization_audit:get_by_resource(Resource, Limit).
macula_authorization_audit:enable().
macula_authorization_audit:disable().
macula_authorization_audit:set_max_entries(N).
macula_authorization_audit:set_retention(Seconds).

Best Practices

Token Lifetime Guidelines

Use CaseRecommended Lifetime
API calls1-24 hours
Treatment periodsDays to weeks
Long-term partnershipsMonths (narrow scope)
Sensitive operationsAlways short
Trial licenses14-30 days
Subscription licensesMatch billing period

Security Recommendations

  1. Use short-lived tokens for sensitive operations
  2. Narrow capability scope - grant only what's needed
  3. Monitor audit logs for unusual patterns
  4. Enable telemetry for real-time observability
  5. Revoke promptly when relationships end
  6. Store UCAN tokens securely (encrypted at rest, treat as credentials)
  7. Always verify Ed25519 signatures on license tokens
  8. Protect private keys - never leave the generating node

Performance Tips

  1. DID cache handles repeated lookups efficiently
  2. Ownership check first - faster than UCAN validation
  3. ETS audit storage can be disabled in production
  4. Telemetry handlers should be non-blocking

Glossary

TermDefinition
DIDDecentralized Identifier - globally unique, self-sovereign identity
UCANUser Controlled Authorization Network - capability-based auth token
CapabilityPermission grant with resource and operation
NamespaceHierarchical identifier path (e.g., io.macula.rgfaber)
CIDContent Identifier - cryptographic hash of content
LRULeast Recently Used - cache eviction strategy
Ed25519Elliptic curve signature algorithm
Realm CertificateSelf-signed root certificate for a Macula realm
Instance CertificateCertificate signed by a realm certificate for a specific node
Trust StoreLocal store of trusted realm certificates

References

Standards

Academic Papers

  • Sporny, M., et al. (2022). Decentralized Identifiers (DIDs) v1.0. W3C Recommendation.
  • Zelenka, B. (2021). UCAN: Trustless, Server-Free Authorization. Fission.
  • Bernstein, D.J., et al. (2012). High-speed high-security signatures. Journal of Cryptographic Engineering.

Implementation Resources