Packages

macula

4.4.4
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_foundation.erl
Raw

src/macula_foundation.erl

%% @doc Foundation trust anchor — firmware-embedded pubkeys for the
%% Macula Foundation FROST-Ed25519 aggregated signer.
%%
%% The Foundation signs Tier A seed lists, protocol parameters, realm
%% trust lists, and T3 attestations (Part 6 §9.14–§9.17). Stations
%% verify those records against a small, firmware-embedded set of
%% foundation pubkeys — the root of trust for the Tier A bootstrap
%% path (Part 5 §4).
%%
%% == Custodians ==
%%
%% Production deployments embed five foundation pubkeys, each held by
%% an independent custodian. FROST m-of-n thresholds keep the signing
%% key resilient: any m custodians can sign, no single custodian can
%% unilaterally authorise a record. See Part 5 §12.
%%
%% == Rotation ==
%%
%% Replacing an embedded pubkey requires a firmware update. The
%% existing record cache survives rotation — records signed by a
%% retired key remain valid until their `valid_until' elapses.
%%
%% == Placeholders ==
%%
%% The five keys returned by `pubkeys/0' at the time of this writing
%% are deterministic <b>placeholders</b> (`&lt;&lt;"macula-v2-foundation-*"&gt;&gt;'
%% SHA-256 digests). They are NOT backed by live FROST shares. Any
%% record signed against a placeholder will fail verification because
%% no corresponding private key exists. Production firmware MUST
%% override `pubkeys/0' via `application:set_env(macula_record,
%% foundation_pubkeys, [...])' before any Tier A record is trusted.
%%
%% Callers that need to operate strictly against the live keys should
%% use `live_pubkeys/0', which returns the app-env override or an
%% empty list (never the placeholders).
%%
%% Reference: plans/PLAN_MACULA_V2_PART5_BOOTSTRAP.md §4, §12;
%% plans/PLAN_MACULA_V2_PART6_PROTOCOL.md §9.14–§9.17.
-module(macula_foundation).
-export([
pubkeys/0,
live_pubkeys/0,
is_foundation/1,
verify_record/1,
placeholder_pubkeys/0,
placeholder_mode/0
]).
-export_type([pubkey/0, verify_error/0]).
-type pubkey() :: macula_identity:pubkey().
-type verify_error() ::
bad_record
| signature_invalid
| expired
| not_foundation_signed
| wrong_type.
-define(PLACEHOLDER_COUNT, 5).
-define(PLACEHOLDER_LABEL_PREFIX, "macula-v2-foundation-placeholder-").
-define(FOUNDATION_TYPES, [16#0D, 16#0E, 16#0F, 16#10]).
%%------------------------------------------------------------------
%% Public API
%%------------------------------------------------------------------
%% @doc Return the current foundation pubkey list.
%%
%% Resolution order:
%% <ol>
%% <li>`application:get_env(macula_record, foundation_pubkeys, [])'
%% if non-empty — production-embedded keys.</li>
%% <li>Deterministic placeholder keys from `placeholder_pubkeys/0'
%% — development / test only.</li>
%% </ol>
-spec pubkeys() -> [pubkey()].
pubkeys() ->
resolve_pubkeys(application:get_env(macula_record, foundation_pubkeys)).
resolve_pubkeys({ok, []}) -> placeholder_pubkeys();
resolve_pubkeys({ok, L}) when is_list(L) -> L;
resolve_pubkeys(undefined) -> placeholder_pubkeys().
%% @doc Return live (non-placeholder) foundation pubkeys, or `[]'.
%%
%% Use this when callers must refuse to trust placeholder keys — e.g.
%% production bootstrap paths that would otherwise accept a record
%% signed by no real private key.
-spec live_pubkeys() -> [pubkey()].
live_pubkeys() ->
case application:get_env(macula_record, foundation_pubkeys) of
{ok, L} when is_list(L), L =/= [] -> L;
_ -> []
end.
%% @doc True iff `Key' is one of the trusted foundation pubkeys.
-spec is_foundation(binary()) -> boolean().
is_foundation(Key) when is_binary(Key), byte_size(Key) =:= 32 ->
lists:member(Key, pubkeys());
is_foundation(_) ->
false.
%% @doc Verify a foundation-signed record.
%%
%% Succeeds iff the record type is one of the foundation tags
%% (`0x0D–0x10'), the envelope `k' field is a trusted foundation
%% pubkey, and the signature + expiry pass
%% `macula_record:verify/1'.
-spec verify_record(macula_record:record()) ->
{ok, macula_record:record()} | {error, verify_error()}.
verify_record(Record) ->
check_type(Record).
check_type(#{type := T} = R) when is_integer(T) ->
case lists:member(T, ?FOUNDATION_TYPES) of
true -> check_key(R);
false -> {error, wrong_type}
end;
check_type(_) ->
{error, bad_record}.
check_key(#{key := K} = R) ->
case is_foundation(K) of
true -> macula_record:verify(R);
false -> {error, not_foundation_signed}
end.
%% @doc Placeholder foundation pubkeys — deterministic, publicly
%% derivable, NOT backed by any private key. Production firmware MUST
%% override via app env before any Tier A record is consumed.
-spec placeholder_pubkeys() -> [pubkey()].
placeholder_pubkeys() ->
[placeholder_key(I) || I <- lists:seq(1, ?PLACEHOLDER_COUNT)].
placeholder_key(I) when is_integer(I), I > 0 ->
Label = iolist_to_binary(
[?PLACEHOLDER_LABEL_PREFIX, integer_to_list(I)]),
crypto:hash(sha256, Label).
%% @doc `true' iff the running station is using placeholder keys.
%%
%% Production code should refuse to bootstrap under `true' — see
%% `live_pubkeys/0'.
-spec placeholder_mode() -> boolean().
placeholder_mode() ->
live_pubkeys() =:= [].