Packages

macula

2.1.1
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 src macula_did_nif.erl
Raw

src/macula_did_nif.erl

%% @doc Decentralized Identifier (DID) operations for Macula mesh.
%%
%% This module provides DID document creation, parsing, and verification
%% for the Macula identity hierarchy. It uses Rust NIFs when available,
%% falling back to pure Erlang implementations otherwise.
%%
%% == DID Format ==
%%
%% Macula uses the `did:macula:` method with hierarchical identities:
%% - Realm: `did:macula:io.macula'
%% - Organization: `did:macula:io.macula.{org}'
%% - Application: `did:macula:io.macula.{org}.{app}'
%%
%% == DID Document Structure ==
%%
%% DID documents follow W3C DID Core specification with Ed25519 keys:
%% ```
%% #{
%% <<"@context">> => [<<"https://www.w3.org/ns/did/v1">>],
%% <<"id">> => <<"did:macula:io.macula.acme">>,
%% <<"controller">> => <<"did:macula:io.macula">>,
%% <<"verificationMethod">> => [...],
%% <<"authentication">> => [...],
%% <<"assertionMethod">> => [...]
%% }
%% '''
%%
%% @author rgfaber
-module(macula_did_nif).
%% API
-export([
create_document/2,
parse_document/1,
extract_public_key/1,
get_did/1,
get_controller/1,
verify_controller/2,
parse_did/1,
is_descendant/2,
is_nif_loaded/0
]).
%% NIF stubs
-export([
nif_create_document/2,
nif_parse_document/1,
nif_extract_public_key/1,
nif_get_did/1,
nif_get_controller/1,
nif_verify_controller/2,
nif_parse_did/1,
nif_is_descendant/2
]).
-on_load(init/0).
-define(NIF_LOADED_KEY, macula_did_nif_loaded).
%%====================================================================
%% Init
%%====================================================================
init() ->
PrivDir = case code:priv_dir(macula) of
{error, _} ->
case code:which(?MODULE) of
Filename when is_list(Filename) ->
filename:join(filename:dirname(filename:dirname(Filename)), "priv");
_ ->
"priv"
end;
Dir ->
Dir
end,
Path = filename:join(PrivDir, "macula_did_nif"),
case erlang:load_nif(Path, 0) of
ok ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, {reload, _}} ->
persistent_term:put(?NIF_LOADED_KEY, true),
ok;
{error, _Reason} ->
%% NIF not available, will use Erlang fallbacks
ok
end.
%%====================================================================
%% API
%%====================================================================
%% @doc Check if the NIF is loaded.
-spec is_nif_loaded() -> boolean().
is_nif_loaded() ->
persistent_term:get(?NIF_LOADED_KEY, false).
%% @doc Create a new DID Document.
%% Returns a JSON-encoded DID document.
-spec create_document(Did :: binary(), PublicKey :: binary()) ->
{ok, DocumentJson :: binary()} | {error, atom()}.
create_document(Did, PublicKey) ->
case is_nif_loaded() of
true -> transform_nif_result(nif_create_document(Did, PublicKey));
false -> erlang_create_document(Did, PublicKey)
end.
%% @doc Parse and validate a DID Document from JSON.
-spec parse_document(DocumentJson :: binary()) ->
{ok, DocumentJson :: binary()} | {error, atom()}.
parse_document(DocumentJson) ->
case is_nif_loaded() of
true -> transform_nif_result(nif_parse_document(DocumentJson));
false -> erlang_parse_document(DocumentJson)
end.
%% @doc Extract the public key from a DID Document.
%% Returns the 32-byte Ed25519 public key.
-spec extract_public_key(DocumentJson :: binary()) ->
{ok, PublicKey :: binary()} | {error, atom()}.
extract_public_key(DocumentJson) ->
case is_nif_loaded() of
true -> transform_nif_result(nif_extract_public_key(DocumentJson));
false -> erlang_extract_public_key(DocumentJson)
end.
%% @doc Get the DID from a DID Document.
-spec get_did(DocumentJson :: binary()) ->
{ok, Did :: binary()} | {error, atom()}.
get_did(DocumentJson) ->
case is_nif_loaded() of
true -> transform_nif_result(nif_get_did(DocumentJson));
false -> erlang_get_did(DocumentJson)
end.
%% @doc Get the controller DID from a DID Document.
%% Returns the controller, or the DID itself if self-controlled.
-spec get_controller(DocumentJson :: binary()) ->
{ok, Controller :: binary()} | {error, atom()}.
get_controller(DocumentJson) ->
case is_nif_loaded() of
true -> transform_nif_result(nif_get_controller(DocumentJson));
false -> erlang_get_controller(DocumentJson)
end.
%% @doc Verify that a DID Document is controlled by the expected controller.
-spec verify_controller(DocumentJson :: binary(), ExpectedController :: binary()) ->
ok | {error, atom()}.
verify_controller(DocumentJson, ExpectedController) ->
case is_nif_loaded() of
true ->
case nif_verify_controller(DocumentJson, ExpectedController) of
ok -> ok;
Err -> {error, Err}
end;
false ->
erlang_verify_controller(DocumentJson, ExpectedController)
end.
%% @doc Parse a DID string and extract its components.
%% Returns a map with method, identity, parts, and depth.
-spec parse_did(Did :: binary()) ->
{ok, Components :: map()} | {error, atom()}.
parse_did(Did) ->
case is_nif_loaded() of
true ->
case nif_parse_did(Did) of
{ok, Json} -> {ok, json_decode(Json)};
{invalid_did, _} -> {error, invalid_did}
end;
false ->
erlang_parse_did(Did)
end.
%% @private Transform NIF result to standard format
transform_nif_result({ok, Result}) -> {ok, Result};
transform_nif_result({invalid_did, _}) -> {error, invalid_did};
transform_nif_result({invalid_public_key, _}) -> {error, invalid_public_key};
transform_nif_result({invalid_document, _}) -> {error, invalid_document};
transform_nif_result({not_found, _}) -> {error, not_found};
transform_nif_result({controller_mismatch, _}) -> {error, controller_mismatch};
transform_nif_result({malformed_json, _}) -> {error, malformed_json}.
%% @doc Check if one DID is a descendant of another.
-spec is_descendant(ChildDid :: binary(), ParentDid :: binary()) -> boolean().
is_descendant(ChildDid, ParentDid) ->
case is_nif_loaded() of
true -> nif_is_descendant(ChildDid, ParentDid);
false -> erlang_is_descendant(ChildDid, ParentDid)
end.
%%====================================================================
%% NIF Stubs (replaced when NIF loads)
%%====================================================================
nif_create_document(_Did, _PublicKey) ->
erlang:nif_error(nif_not_loaded).
nif_parse_document(_DocumentJson) ->
erlang:nif_error(nif_not_loaded).
nif_extract_public_key(_DocumentJson) ->
erlang:nif_error(nif_not_loaded).
nif_get_did(_DocumentJson) ->
erlang:nif_error(nif_not_loaded).
nif_get_controller(_DocumentJson) ->
erlang:nif_error(nif_not_loaded).
nif_verify_controller(_DocumentJson, _ExpectedController) ->
erlang:nif_error(nif_not_loaded).
nif_parse_did(_Did) ->
erlang:nif_error(nif_not_loaded).
nif_is_descendant(_ChildDid, _ParentDid) ->
erlang:nif_error(nif_not_loaded).
%%====================================================================
%% Pure Erlang Fallbacks
%%====================================================================
%% @private Create DID document using pure Erlang
erlang_create_document(Did, PublicKey) when byte_size(PublicKey) =:= 32 ->
case binary:match(Did, <<"did:macula:">>) of
{0, _} ->
DidStr = binary_to_list(Did),
KeyId = <<Did/binary, "#key-1">>,
%% Encode public key as multibase (z + base58btc of multicodec + key)
MulticodecKey = <<16#ed, 16#01, PublicKey/binary>>,
MultibaseKey = <<"z", (base58_encode(MulticodecKey))/binary>>,
%% Determine controller (parent in hierarchy)
Controller = get_parent_did_erlang(DidStr),
Doc = #{
<<"@context">> => [<<"https://www.w3.org/ns/did/v1">>],
<<"id">> => Did,
<<"controller">> => Controller,
<<"verificationMethod">> => [#{
<<"id">> => KeyId,
<<"type">> => <<"Ed25519VerificationKey2020">>,
<<"controller">> => Did,
<<"publicKeyMultibase">> => MultibaseKey
}],
<<"authentication">> => [KeyId],
<<"assertionMethod">> => [KeyId],
<<"capabilityInvocation">> => [KeyId],
<<"capabilityDelegation">> => [KeyId]
},
%% Remove null controller for root
Doc2 = case Controller of
null -> maps:remove(<<"controller">>, Doc);
_ -> Doc
end,
{ok, json_encode(Doc2)};
_ ->
{error, invalid_did}
end;
erlang_create_document(_Did, _PublicKey) ->
{error, invalid_public_key}.
%% @private Parse DID document
erlang_parse_document(DocumentJson) ->
try
_ = json_decode(DocumentJson),
{ok, DocumentJson}
catch
_:_ -> {error, invalid_document}
end.
%% @private Extract public key from DID document
erlang_extract_public_key(DocumentJson) ->
try
Doc = json_decode(DocumentJson),
[VM | _] = maps:get(<<"verificationMethod">>, Doc),
MultibaseKey = maps:get(<<"publicKeyMultibase">>, VM),
<<"z", Base58Key/binary>> = MultibaseKey,
Decoded = base58_decode(Base58Key),
%% Check multicodec prefix (0xed01 for Ed25519)
<<16#ed, 16#01, PublicKey:32/binary>> = Decoded,
{ok, PublicKey}
catch
_:_ -> {error, invalid_document}
end.
%% @private Get DID from document
erlang_get_did(DocumentJson) ->
try
Doc = json_decode(DocumentJson),
{ok, maps:get(<<"id">>, Doc)}
catch
_:_ -> {error, invalid_document}
end.
%% @private Get controller from document
erlang_get_controller(DocumentJson) ->
try
Doc = json_decode(DocumentJson),
Did = maps:get(<<"id">>, Doc),
Controller = maps:get(<<"controller">>, Doc, Did),
{ok, Controller}
catch
_:_ -> {error, invalid_document}
end.
%% @private Verify controller
erlang_verify_controller(DocumentJson, ExpectedController) ->
case erlang_get_controller(DocumentJson) of
{ok, ExpectedController} -> ok;
{ok, _Other} -> {error, controller_mismatch};
Error -> Error
end.
%% @private Parse DID string
erlang_parse_did(Did) ->
case binary:split(Did, <<":">>, [global]) of
[<<"did">>, <<"macula">>, Identity] ->
Parts = binary:split(Identity, <<".">>, [global]),
{ok, #{
<<"method">> => <<"macula">>,
<<"identity">> => Identity,
<<"parts">> => Parts,
<<"depth">> => length(Parts)
}};
_ ->
{error, invalid_did}
end.
%% @private Check if child is descendant of parent
erlang_is_descendant(ChildDid, ParentDid) ->
case {binary:match(ChildDid, <<"did:macula:">>),
binary:match(ParentDid, <<"did:macula:">>)} of
{{0, 11}, {0, 11}} ->
ChildIdentity = binary:part(ChildDid, 11, byte_size(ChildDid) - 11),
ParentIdentity = binary:part(ParentDid, 11, byte_size(ParentDid) - 11),
case ChildIdentity =:= ParentIdentity of
true -> false; % Same DID
false ->
Prefix = <<ParentIdentity/binary, ".">>,
case binary:match(ChildIdentity, Prefix) of
{0, _} -> true;
_ -> false
end
end;
_ ->
false
end.
%% @private Get parent DID
get_parent_did_erlang(DidStr) ->
case string:prefix(DidStr, "did:macula:") of
nomatch -> null;
Identity ->
Parts = string:split(Identity, ".", all),
case length(Parts) of
N when N =< 2 -> null; % Root realm has no parent
_ ->
ParentParts = lists:droplast(Parts),
ParentIdentity = string:join(ParentParts, "."),
list_to_binary("did:macula:" ++ ParentIdentity)
end
end.
%%====================================================================
%% JSON Helpers (minimal implementation)
%%====================================================================
%% @private Encode Erlang term to JSON binary
json_encode(Term) ->
iolist_to_binary(encode_value(Term)).
encode_value(null) -> <<"null">>;
encode_value(true) -> <<"true">>;
encode_value(false) -> <<"false">>;
encode_value(N) when is_integer(N) -> integer_to_binary(N);
encode_value(N) when is_float(N) -> float_to_binary(N, [{decimals, 10}, compact]);
encode_value(S) when is_binary(S) -> encode_string(S);
encode_value(L) when is_list(L) -> encode_array(L);
encode_value(M) when is_map(M) -> encode_object(M).
encode_string(S) ->
Escaped = binary:replace(
binary:replace(
binary:replace(
binary:replace(S, <<"\\">>, <<"\\\\">>, [global]),
<<"\"">>, <<"\\\"">>, [global]),
<<"\n">>, <<"\\n">>, [global]),
<<"\t">>, <<"\\t">>, [global]),
<<"\"", Escaped/binary, "\"">>.
encode_array(L) ->
Elements = lists:map(fun encode_value/1, L),
<<"[", (iolist_to_binary(lists:join(<<",">>, Elements)))/binary, "]">>.
encode_object(M) ->
Pairs = maps:fold(fun(K, V, Acc) ->
[<<(encode_string(K))/binary, ":", (iolist_to_binary(encode_value(V)))/binary>> | Acc]
end, [], M),
<<"{", (iolist_to_binary(lists:join(<<",">>, Pairs)))/binary, "}">>.
%% @private Decode JSON binary to Erlang term
json_decode(Bin) ->
{Value, _Rest} = decode_value(skip_ws(Bin)),
Value.
decode_value(<<"null", Rest/binary>>) -> {null, Rest};
decode_value(<<"true", Rest/binary>>) -> {true, Rest};
decode_value(<<"false", Rest/binary>>) -> {false, Rest};
decode_value(<<"\"", Rest/binary>>) -> decode_string(Rest, <<>>);
decode_value(<<"[", Rest/binary>>) -> decode_array(skip_ws(Rest), []);
decode_value(<<"{", Rest/binary>>) -> decode_object(skip_ws(Rest), #{});
decode_value(<<C, _/binary>> = Bin) when C =:= $- orelse (C >= $0 andalso C =< $9) ->
decode_number(Bin).
decode_string(<<"\\\"", Rest/binary>>, Acc) -> decode_string(Rest, <<Acc/binary, "\"">>);
decode_string(<<"\\\\", Rest/binary>>, Acc) -> decode_string(Rest, <<Acc/binary, "\\">>);
decode_string(<<"\\n", Rest/binary>>, Acc) -> decode_string(Rest, <<Acc/binary, "\n">>);
decode_string(<<"\\t", Rest/binary>>, Acc) -> decode_string(Rest, <<Acc/binary, "\t">>);
decode_string(<<"\"", Rest/binary>>, Acc) -> {Acc, Rest};
decode_string(<<C, Rest/binary>>, Acc) -> decode_string(Rest, <<Acc/binary, C>>).
decode_array(<<"]", Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest};
decode_array(Bin, Acc) ->
{Value, Rest} = decode_value(Bin),
Rest2 = skip_ws(Rest),
case Rest2 of
<<",", Rest3/binary>> -> decode_array(skip_ws(Rest3), [Value | Acc]);
<<"]", Rest3/binary>> -> {lists:reverse([Value | Acc]), Rest3}
end.
decode_object(<<"}", Rest/binary>>, Acc) -> {Acc, Rest};
decode_object(<<"\"", Rest/binary>>, Acc) ->
{Key, Rest2} = decode_string(Rest, <<>>),
<<":", Rest3/binary>> = skip_ws(Rest2),
{Value, Rest4} = decode_value(skip_ws(Rest3)),
Rest5 = skip_ws(Rest4),
case Rest5 of
<<",", Rest6/binary>> -> decode_object(skip_ws(Rest6), maps:put(Key, Value, Acc));
<<"}", Rest6/binary>> -> {maps:put(Key, Value, Acc), Rest6}
end.
decode_number(Bin) ->
{NumStr, Rest} = take_number(Bin, <<>>),
Num = case binary:match(NumStr, [<<".">>, <<"e">>, <<"E">>]) of
nomatch -> binary_to_integer(NumStr);
_ -> binary_to_float(NumStr)
end,
{Num, Rest}.
take_number(<<C, Rest/binary>>, Acc) when C =:= $- orelse C =:= $+ orelse C =:= $.
orelse C =:= $e orelse C =:= $E orelse (C >= $0 andalso C =< $9) ->
take_number(Rest, <<Acc/binary, C>>);
take_number(Rest, Acc) -> {Acc, Rest}.
skip_ws(<<C, Rest/binary>>) when C =:= $ orelse C =:= $\t orelse C =:= $\n orelse C =:= $\r ->
skip_ws(Rest);
skip_ws(Bin) -> Bin.
%%====================================================================
%% Base58 Helpers (Bitcoin alphabet)
%%====================================================================
-define(BASE58_ALPHABET, "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz").
base58_encode(Bin) ->
LeadingZeros = count_leading_zeros(Bin, 0),
Ones = list_to_binary(lists:duplicate(LeadingZeros, $1)),
Encoded = base58_encode_int(binary:decode_unsigned(Bin, big), <<>>),
<<Ones/binary, Encoded/binary>>.
base58_encode_int(0, Acc) -> Acc;
base58_encode_int(N, Acc) ->
Char = lists:nth((N rem 58) + 1, ?BASE58_ALPHABET),
base58_encode_int(N div 58, <<Char, Acc/binary>>).
count_leading_zeros(<<0, Rest/binary>>, Count) -> count_leading_zeros(Rest, Count + 1);
count_leading_zeros(_, Count) -> Count.
base58_decode(Bin) ->
LeadingOnes = count_leading_ones(Bin, 0),
Zeros = binary:copy(<<0>>, LeadingOnes),
Decoded = base58_decode_int(Bin, 0),
DecodedBin = binary:encode_unsigned(Decoded, big),
<<Zeros/binary, DecodedBin/binary>>.
base58_decode_int(<<>>, Acc) -> Acc;
base58_decode_int(<<C, Rest/binary>>, Acc) ->
Index = string:chr(?BASE58_ALPHABET, C) - 1,
base58_decode_int(Rest, Acc * 58 + Index).
count_leading_ones(<<$1, Rest/binary>>, Count) -> count_leading_ones(Rest, Count + 1);
count_leading_ones(_, Count) -> Count.