Packages

macula

0.20.11
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_gatekeeper.erl
Raw

src/macula_gatekeeper.erl

%% @doc Gatekeeper module for validating mesh application admissions.
%%
%% == Overview ==
%%
%% The gatekeeper validates that applications are "mesh-worthy" before allowing
%% them to participate in the Macula mesh network. Validation happens at:
%% - Session establishment (initial admission)
%% - Periodically during session (health checks)
%% - On each operation (capability enforcement)
%%
%% == Validation Layers ==
%%
%% 1. Protocol Compliance: App implements `macula_protocol' behaviour
%% 2. Identity Verification: Identity matches presented certificate
%% 3. Certificate Validation: Certificate is valid, not expired, not revoked
%% 4. Capability Declaration: App declares its required capabilities
%% 5. Health Status: App responds to health checks
%%
%% == BEAM vs Non-BEAM Apps ==
%%
%% For BEAM apps (Erlang/Elixir):
%% - Use `verify_beam_app/2' which checks `code:ensure_loaded/1'
%% - Validates behaviour callbacks via module introspection
%%
%% For non-BEAM apps (via sidecar or gRPC):
%% - Use `verify_external_app/2' which probes HTTP/gRPC endpoints
%% - Requires macula sidecar or compatible protocol implementation
%%
%% @see macula_protocol
%% @see macula_authorization
%% @author Macula Team
%% @end
-module(macula_gatekeeper).
-include_lib("kernel/include/logger.hrl").
%%====================================================================
%% Types
%%====================================================================
-type identity() :: binary().
-type certificate() :: binary(). % PEM-encoded
-type validation_result() :: {ok, app_manifest()} | {error, validation_error()}.
-type app_manifest() :: #{
identity := identity(),
capabilities := [macula_protocol:capability()],
api := macula_protocol:api_spec(),
certificate_fingerprint := binary(),
verified_at := calendar:datetime()
}.
-type validation_error() ::
no_macula_sdk |
not_macula_app |
behaviour_not_implemented |
identity_mismatch |
certificate_invalid |
certificate_expired |
certificate_revoked |
health_check_failed |
{validation_exception, term()}.
-export_type([app_manifest/0, validation_result/0, validation_error/0]).
%%====================================================================
%% API
%%====================================================================
-export([
verify_beam_app/2,
verify_beam_app/3,
verify_external_app/2,
verify_certificate/2,
check_health/1,
validate_operation/3,
%% Exported for RPC calls from remote nodes
verify_callbacks/1
]).
%%====================================================================
%% BEAM App Verification
%%====================================================================
%% @doc Verifies a BEAM app is mesh-worthy.
-spec verify_beam_app(module(), certificate()) -> validation_result().
verify_beam_app(Module, CertPem) ->
verify_beam_app(Module, CertPem, node()).
-spec verify_beam_app(module(), certificate(), node()) -> validation_result().
verify_beam_app(Module, CertPem, Node) ->
?LOG_DEBUG("Gatekeeper: verifying BEAM app ~p on node ~p", [Module, Node]),
Result = run_verification_pipeline(Module, CertPem, Node),
audit_verification(Module, Node, Result),
Result.
%% Pipeline approach - each step returns ok or error, chain with andalso-like logic
run_verification_pipeline(Module, CertPem, Node) ->
with_step(ensure_protocol_loaded(Node), fun() ->
with_step(check_behaviour_implementation(Module, Node), fun() ->
with_step(get_and_validate_identity(Module, CertPem, Node), fun({Identity, Fingerprint}) ->
with_step(get_capabilities_and_api(Module, Node), fun({Caps, Api}) ->
with_step(check_app_health(Module, Node), fun() ->
{ok, #{
identity => Identity,
capabilities => Caps,
api => Api,
certificate_fingerprint => Fingerprint,
verified_at => calendar:universal_time()
}}
end)end)end)end)end).
%% Helper for pipeline - continues on ok, stops on error
with_step({ok, Value}, Next) -> Next(Value);
with_step(ok, Next) -> Next();
with_step({error, _} = Err, _Next) -> Err.
%%====================================================================
%% External App Verification
%%====================================================================
%% @doc Verifies a non-BEAM app via HTTP/gRPC probes.
-spec verify_external_app(uri_string:uri_string(), certificate()) -> validation_result().
verify_external_app(Endpoint, _CertPem) ->
?LOG_WARNING("External app verification not yet implemented for ~s", [Endpoint]),
{error, {not_implemented, external_verification}}.
%%====================================================================
%% Certificate Verification
%%====================================================================
%% @doc Verifies a certificate and extracts identity.
-spec verify_certificate(certificate(), identity()) ->
{ok, binary()} | {error, validation_error()}.
verify_certificate(CertPem, ExpectedIdentity) when is_binary(CertPem) ->
case decode_certificate(CertPem) of
{ok, Cert} ->
verify_certificate_chain(Cert, CertPem, ExpectedIdentity);
{error, Reason} ->
{error, Reason}
end;
verify_certificate(_, _) ->
{error, {certificate_invalid, not_binary}}.
verify_certificate_chain(Cert, CertPem, ExpectedIdentity) ->
case check_certificate_expiry(Cert) of
ok ->
verify_certificate_identity(Cert, CertPem, ExpectedIdentity);
{error, _} = Err ->
Err
end.
verify_certificate_identity(_Cert, CertPem, _ExpectedIdentity) ->
%% For now, skip identity extraction from cert (complex ASN.1 parsing)
%% In production, extract CN and compare to ExpectedIdentity
Fingerprint = calculate_fingerprint(CertPem),
{ok, Fingerprint}.
%%====================================================================
%% Health Checks
%%====================================================================
%% @doc Performs a health check on a verified app.
-spec check_health(app_manifest()) -> ok | {error, term()}.
check_health(#{identity := Identity}) ->
?LOG_DEBUG("Health check for ~s", [Identity]),
ok.
%%====================================================================
%% Operation Validation
%%====================================================================
%% @doc Validates that an app can perform an operation.
-spec validate_operation(app_manifest(), atom(), binary()) -> ok | {error, term()}.
validate_operation(#{capabilities := Caps}, Operation, _Resource) ->
validate_capability(Caps, Operation).
validate_capability(Caps, Operation) when
Operation =:= publish;
Operation =:= subscribe;
Operation =:= call;
Operation =:= register;
Operation =:= provide_content;
Operation =:= consume_content ->
case lists:member(Operation, Caps) of
true -> ok;
false -> {error, {capability_not_declared, Operation}}
end;
validate_capability(_, Operation) ->
{error, {unknown_operation, Operation}}.
%%====================================================================
%% Internal: Protocol Loading
%%====================================================================
ensure_protocol_loaded(Node) when Node =:= node() ->
ensure_module_loaded(macula_protocol);
ensure_protocol_loaded(Node) ->
rpc_ensure_loaded(Node, macula_protocol).
ensure_module_loaded(Module) ->
case code:ensure_loaded(Module) of
{module, Module} -> ok;
_ -> {error, no_macula_sdk}
end.
rpc_ensure_loaded(Node, Module) ->
case rpc:call(Node, code, ensure_loaded, [Module]) of
{module, Module} -> ok;
{badrpc, Reason} -> {error, {rpc_failed, Reason}};
_ -> {error, no_macula_sdk}
end.
%%====================================================================
%% Internal: Behaviour Checking
%%====================================================================
check_behaviour_implementation(Module, Node) when Node =:= node() ->
case code:ensure_loaded(Module) of
{module, Module} -> verify_callbacks(Module);
_ -> {error, {module_not_found, Module}}
end;
check_behaviour_implementation(Module, Node) ->
case rpc:call(Node, code, ensure_loaded, [Module]) of
{module, Module} -> rpc_verify_callbacks(Node, Module);
{badrpc, Reason} -> {error, {rpc_failed, Reason}};
_ -> {error, {module_not_found, Module}}
end.
rpc_verify_callbacks(Node, Module) ->
case rpc:call(Node, ?MODULE, verify_callbacks, [Module]) of
ok -> ok;
{error, _} = Err -> Err;
{badrpc, Reason} -> {error, {rpc_failed, Reason}}
end.
-spec verify_callbacks(module()) -> ok | {error, term()}.
verify_callbacks(Module) ->
RequiredCallbacks = [
{mesh_identity, 0},
{mesh_capabilities, 0},
{mesh_api, 0},
{handle_mesh_event, 2},
{handle_rpc_call, 2},
{provide_content, 1},
{content_received, 2},
{mesh_health, 0}
],
Exports = Module:module_info(exports),
Missing = [CB || CB <- RequiredCallbacks, not lists:member(CB, Exports)],
case Missing of
[] -> ok;
_ -> {error, {missing_callbacks, Missing}}
end.
%%====================================================================
%% Internal: Identity Validation
%%====================================================================
get_and_validate_identity(Module, CertPem, Node) when Node =:= node() ->
Identity = Module:mesh_identity(),
validate_identity_with_cert(Identity, CertPem);
get_and_validate_identity(Module, CertPem, Node) ->
case rpc:call(Node, Module, mesh_identity, []) of
Identity when is_binary(Identity) ->
validate_identity_with_cert(Identity, CertPem);
{badrpc, Reason} ->
{error, {rpc_failed, Reason}}
end.
validate_identity_with_cert(Identity, CertPem) ->
case verify_certificate(CertPem, Identity) of
{ok, Fingerprint} -> {ok, {Identity, Fingerprint}};
{error, _} = Err -> Err
end.
%%====================================================================
%% Internal: Capabilities and API
%%====================================================================
get_capabilities_and_api(Module, Node) when Node =:= node() ->
Caps = Module:mesh_capabilities(),
Api = Module:mesh_api(),
validate_caps_and_api(Caps, Api);
get_capabilities_and_api(Module, Node) ->
get_caps_and_api_remote(Module, Node).
get_caps_and_api_remote(Module, Node) ->
case rpc:call(Node, Module, mesh_capabilities, []) of
Caps when is_list(Caps) ->
case rpc:call(Node, Module, mesh_api, []) of
Api when is_map(Api) ->
validate_caps_and_api(Caps, Api);
{badrpc, Reason} ->
{error, {rpc_failed, Reason}}
end;
{badrpc, Reason} ->
{error, {rpc_failed, Reason}}
end.
validate_caps_and_api(Caps, Api) ->
case macula_protocol:validate_capabilities(Caps) of
ok ->
case macula_protocol:validate_api_spec(Api) of
ok -> {ok, {Caps, Api}};
{error, _} = Err -> Err
end;
{error, _} = Err ->
Err
end.
%%====================================================================
%% Internal: Health Check
%%====================================================================
check_app_health(Module, Node) when Node =:= node() ->
Module:mesh_health();
check_app_health(Module, Node) ->
case rpc:call(Node, Module, mesh_health, []) of
ok -> ok;
{error, _} = Err -> Err;
{badrpc, Reason} -> {error, {rpc_failed, Reason}}
end.
%%====================================================================
%% Internal: Certificate Utilities
%%====================================================================
decode_certificate(CertPem) ->
case public_key:pem_decode(CertPem) of
[{'Certificate', DerCert, _}] ->
decode_der_certificate(DerCert);
[] ->
{error, {certificate_invalid, empty_pem}};
_ ->
{error, {certificate_invalid, decode_failed}}
end.
decode_der_certificate(<<>>) ->
{error, {certificate_invalid, empty_der}};
decode_der_certificate(DerCert) when byte_size(DerCert) < 50 ->
%% Valid X.509 certs are at least ~200 bytes; 50 is generous minimum
{error, {certificate_invalid, invalid_der}};
decode_der_certificate(<<16#30, _/binary>> = DerCert) ->
%% Starts with SEQUENCE tag (0x30) - valid DER structure
{ok, public_key:pkix_decode_cert(DerCert, otp)};
decode_der_certificate(_DerCert) ->
%% Does not start with SEQUENCE tag - not a valid certificate
{error, {certificate_invalid, invalid_der_structure}}.
check_certificate_expiry(_Cert) ->
%% TODO: Extract validity dates and check against current time
ok.
calculate_fingerprint(CertPem) ->
Hash = crypto:hash(sha256, CertPem),
list_to_binary([io_lib:format("~2.16.0B", [B]) || <<B>> <= Hash]).
%%====================================================================
%% Internal: Audit Logging
%%====================================================================
audit_verification(Module, Node, {ok, Manifest}) ->
?LOG_INFO("Gatekeeper: ADMITTED app=~p node=~p identity=~s caps=~p",
[Module, Node, maps:get(identity, Manifest), maps:get(capabilities, Manifest)]);
audit_verification(Module, Node, {error, Reason}) ->
?LOG_WARNING("Gatekeeper: REJECTED app=~p node=~p reason=~p",
[Module, Node, Reason]).