Packages

macula

3.10.2
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 src macula_cert_system macula_trust_store.erl
Raw

src/macula_cert_system/macula_trust_store.erl

%%%-------------------------------------------------------------------
%%% @doc Macula Certificate Trust Store
%%%
%%% ETS-based storage for trusted realm certificates.
%%% Manages trust decisions for self-sovereign certificates.
%%%
%%% Trust Model:
%%% - Realm certificates are added to the trust store explicitly
%%% - Instance certificates are verified against their realm's certificate
%%% - Trust-on-first-use (TOFU) can be enabled for automatic trust
%%%
%%% Example usage:
%%% ```
%%% %% Start the trust store
%%% {ok, Pid} = macula_trust_store:start_link(),
%%%
%%% %% Add a trusted realm certificate
%%% ok = macula_trust_store:add_trusted_realm(RealmDID, RealmCert),
%%%
%%% %% Check if a realm is trusted
%%% true = macula_trust_store:is_trusted(RealmDID),
%%%
%%% %% Get the certificate for verification
%%% {ok, RealmCert} = macula_trust_store:get_realm_cert(RealmDID),
%%%
%%% %% Verify an instance certificate
%%% ok = macula_trust_store:verify_instance_cert(InstanceCert).
%%% '''
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_trust_store).
-behaviour(gen_server).
-include("macula_cert.hrl").
%% API
-export([start_link/0, start_link/1]).
-export([add_trusted_realm/2, add_trusted_realm/3]).
-export([remove_trusted_realm/1]).
-export([is_trusted/1]).
-export([get_realm_cert/1]).
-export([list_trusted/0]).
-export([verify_instance_cert/1]).
-export([clear_all/0]).
-export([count/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
%% ETS table name
-define(TABLE, macula_trust_store).
%% Default options
-define(DEFAULT_OPTS, #{
tofu => false, %% Trust-on-first-use disabled by default
persist => false %% No persistence by default
}).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Start the trust store with default options
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
start_link(?DEFAULT_OPTS).
%% @doc Start the trust store with custom options
-spec start_link(Opts :: map()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
%% @doc Add a realm certificate to the trust store
-spec add_trusted_realm(RealmDID :: binary(), Cert :: macula_cert()) ->
ok | {error, term()}.
add_trusted_realm(RealmDID, Cert) ->
add_trusted_realm(RealmDID, Cert, <<>>).
%% @doc Add a realm certificate with optional notes
-spec add_trusted_realm(RealmDID :: binary(), Cert :: macula_cert(), Notes :: binary()) ->
ok | {error, term()}.
add_trusted_realm(RealmDID, Cert, Notes) ->
gen_server:call(?MODULE, {add_trusted, RealmDID, Cert, Notes}).
%% @doc Remove a realm from the trust store
-spec remove_trusted_realm(RealmDID :: binary()) -> ok | {error, not_found}.
remove_trusted_realm(RealmDID) ->
gen_server:call(?MODULE, {remove_trusted, RealmDID}).
%% @doc Check if a realm DID is in the trust store
-spec is_trusted(RealmDID :: binary()) -> boolean().
is_trusted(RealmDID) ->
case ets:lookup(?TABLE, RealmDID) of
[_] -> true;
[] -> false
end.
%% @doc Get the certificate for a trusted realm
-spec get_realm_cert(RealmDID :: binary()) -> {ok, macula_cert()} | {error, not_found}.
get_realm_cert(RealmDID) ->
case ets:lookup(?TABLE, RealmDID) of
[{_, Entry}] ->
{ok, Entry#trust_entry.cert};
[] ->
{error, not_found}
end.
%% @doc List all trusted realm DIDs
-spec list_trusted() -> [binary()].
list_trusted() ->
ets:foldl(
fun({DID, _Entry}, Acc) -> [DID | Acc] end,
[],
?TABLE
).
%% @doc Verify an instance certificate against the trust store
%% Looks up the issuer's realm certificate and verifies the chain.
-spec verify_instance_cert(Cert :: macula_cert()) -> ok | {error, term()}.
verify_instance_cert(Cert) ->
#macula_cert{issuer_did = IssuerDID} = Cert,
case get_realm_cert(IssuerDID) of
{ok, RealmCert} ->
macula_cert:verify_cert(Cert, RealmCert);
{error, not_found} ->
{error, {issuer_not_trusted, IssuerDID}}
end.
%% @doc Clear all trusted realms (use with caution!)
-spec clear_all() -> ok.
clear_all() ->
gen_server:call(?MODULE, clear_all).
%% @doc Get the count of trusted realms
-spec count() -> non_neg_integer().
count() ->
ets:info(?TABLE, size).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
init(Opts) ->
%% Create ETS table
?TABLE = ets:new(?TABLE, [
named_table,
public,
{read_concurrency, true},
{keypos, 1}
]),
%% Load persisted trust entries if enabled
case maps:get(persist, Opts, false) of
true ->
load_persisted_entries(Opts);
false ->
ok
end,
{ok, #{opts => Opts}}.
%% @private
handle_call({add_trusted, RealmDID, Cert, Notes}, _From, State) ->
Result = do_add_trusted(RealmDID, Cert, Notes),
{reply, Result, State};
handle_call({remove_trusted, RealmDID}, _From, State) ->
Result = case ets:lookup(?TABLE, RealmDID) of
[_] ->
ets:delete(?TABLE, RealmDID),
ok;
[] ->
{error, not_found}
end,
{reply, Result, State};
handle_call(clear_all, _From, State) ->
ets:delete_all_objects(?TABLE),
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
%% @private
handle_cast(_Msg, State) ->
{noreply, State}.
%% @private
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
ok.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @private Add a trusted realm after validation
-spec do_add_trusted(RealmDID :: binary(), Cert :: macula_cert(), Notes :: binary()) ->
ok | {error, term()}.
do_add_trusted(RealmDID, Cert, Notes) ->
#macula_cert{subject_did = SubjectDID} = Cert,
%% Verify the certificate is for the claimed realm
case SubjectDID =:= RealmDID of
true ->
%% Verify it's a valid self-signed certificate
case macula_cert:verify_self_signed(Cert) of
ok ->
Entry = #trust_entry{
realm_did = RealmDID,
cert = Cert,
added_at = erlang:system_time(second),
verified = true,
notes = Notes
},
ets:insert(?TABLE, {RealmDID, Entry}),
ok;
{error, _} = Error ->
Error
end;
false ->
{error, {did_mismatch, RealmDID, SubjectDID}}
end.
%% @private Load persisted trust entries from disk
-spec load_persisted_entries(Opts :: map()) -> ok.
load_persisted_entries(Opts) ->
load_from_path(maps:get(persist_path, Opts, undefined)).
%% @private No persist path configured
load_from_path(undefined) ->
ok;
%% @private Load entries from file
load_from_path(Path) ->
handle_file_read(file:read_file(Path)).
%% @private File read failed
handle_file_read({error, _}) ->
ok;
%% @private File read succeeded - decode entries
handle_file_read({ok, Binary}) ->
insert_decoded_entries(catch binary_to_term(Binary, [safe])).
%% @private Decoding failed
insert_decoded_entries({'EXIT', _}) ->
ok;
%% @private Insert entries into ETS
insert_decoded_entries(Entries) when is_list(Entries) ->
lists:foreach(
fun({DID, Entry}) ->
ets:insert(?TABLE, {DID, Entry})
end,
Entries
);
insert_decoded_entries(_) ->
ok.