Packages
macula
4.7.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
Current section
Files
src/identity/macula_identity.erl
%% @doc Ed25519 identities and the S/Kademlia crypto puzzle.
%%
%% A Macula NodeId is an Ed25519 public key (32 bytes). Identities are
%% optionally "puzzle-hardened": the pubkey satisfies
%% `SHA-256(pubkey)' having at least N leading zero bits. This raises the
%% cost of mass identity minting (Sybil defence).
%%
%% See `plans/PLAN_MACULA_V2_PART1_FOUNDATIONS.md' sections 4.1–4.4.
-module(macula_identity).
-export([
generate/0,
generate/1,
load/1,
save/2,
public/1,
private/1,
node_id/1,
sign/2,
verify/3,
puzzle_evidence/1,
puzzle_valid/1,
puzzle_valid/2
]).
-export_type([pubkey/0, privkey/0, sig/0, key_pair/0, node_id/0]).
-type pubkey() :: <<_:256>>.
-type privkey() :: <<_:256>>.
-type sig() :: <<_:512>>.
-type node_id() :: pubkey().
-type key_pair() :: #{public := pubkey(), private := privkey()}.
-define(KEY_FILE_MAGIC, "macula-v2-key\0").
-define(DEFAULT_PUZZLE_DIFFICULTY, 8).
%%------------------------------------------------------------------
%% Generation
%%------------------------------------------------------------------
%% @doc Generate a fresh Ed25519 key pair. Does not grind a puzzle.
-spec generate() -> key_pair().
generate() ->
{Pub, Priv} = crypto:generate_key(eddsa, ed25519),
#{public => Pub, private => Priv}.
%% @doc Generate a key pair, optionally grinding until the puzzle is satisfied.
%%
%% Opts:
%% <ul>
%% <li>`puzzle' :: boolean() — default false</li>
%% <li>`difficulty' :: non_neg_integer() — leading zero bits required</li>
%% </ul>
-spec generate(#{puzzle => boolean(), difficulty => non_neg_integer(), _ => _}) ->
key_pair().
generate(#{puzzle := true} = Opts) ->
Difficulty = maps:get(difficulty, Opts, default_difficulty()),
grind(Difficulty);
generate(_Opts) ->
generate().
%%------------------------------------------------------------------
%% Persistence — atomic write with 0600 permissions.
%%------------------------------------------------------------------
%% @doc Load a key pair from disk.
-spec load(file:name_all()) -> {ok, key_pair()} | {error, term()}.
load(Path) ->
decode_key_file(file:read_file(Path)).
-spec decode_key_file({ok, binary()} | {error, term()}) ->
{ok, key_pair()} | {error, term()}.
decode_key_file({ok, <<?KEY_FILE_MAGIC, Pub:32/binary, Priv:32/binary>>}) ->
{ok, #{public => Pub, private => Priv}};
decode_key_file({ok, _Blob}) ->
{error, bad_key_file};
decode_key_file({error, _} = Err) ->
Err.
%% @doc Save a key pair to disk atomically (write-tmp + rename) with 0600 perms.
-spec save(file:name_all(), key_pair()) -> ok | {error, term()}.
save(Path, #{public := Pub, private := Priv})
when byte_size(Pub) =:= 32, byte_size(Priv) =:= 32 ->
Blob = <<?KEY_FILE_MAGIC, Pub/binary, Priv/binary>>,
Tmp = iolist_to_binary([Path, ".tmp"]),
ok = filelib:ensure_dir(Path),
write_and_rename(Tmp, Path, Blob).
-spec write_and_rename(file:name_all(), file:name_all(), binary()) ->
ok | {error, term()}.
write_and_rename(Tmp, Path, Blob) ->
case file:write_file(Tmp, Blob, [raw, binary]) of
ok -> finalise_key_file(Tmp, Path);
{error, _} = E -> E
end.
-spec finalise_key_file(file:name_all(), file:name_all()) -> ok | {error, term()}.
finalise_key_file(Tmp, Path) ->
_ = file:change_mode(Tmp, 8#0600),
file:rename(Tmp, Path).
%%------------------------------------------------------------------
%% Accessors
%%------------------------------------------------------------------
-spec public(key_pair()) -> pubkey().
public(#{public := Pub}) -> Pub.
-spec private(key_pair()) -> privkey().
private(#{private := Priv}) -> Priv.
%% @doc NodeId of an identity. Phase 1: NodeId == public key.
-spec node_id(key_pair() | pubkey()) -> node_id().
node_id(#{public := Pub}) -> Pub;
node_id(Pub) when is_binary(Pub), byte_size(Pub) =:= 32 -> Pub.
%%------------------------------------------------------------------
%% Sign / verify — raw Ed25519. Callers add domain separation
%% (see hecate_record, hecate_frame).
%%------------------------------------------------------------------
-spec sign(iodata(), key_pair() | privkey()) -> sig().
sign(Msg, #{private := Priv}) ->
sign(Msg, Priv);
sign(Msg, Priv) when is_binary(Priv), byte_size(Priv) =:= 32 ->
crypto:sign(eddsa, none, Msg, [Priv, ed25519]).
-spec verify(iodata(), sig(), pubkey()) -> boolean().
verify(Msg, Sig, Pub)
when is_binary(Sig), byte_size(Sig) =:= 64,
is_binary(Pub), byte_size(Pub) =:= 32 ->
crypto:verify(eddsa, none, Msg, Sig, [Pub, ed25519]).
%%------------------------------------------------------------------
%% Crypto puzzle (S/Kademlia Sybil defence).
%%------------------------------------------------------------------
%% @doc SHA-256 of the public key — the proof-of-work output measured.
-spec puzzle_evidence(pubkey() | key_pair()) -> <<_:256>>.
puzzle_evidence(#{public := Pub}) ->
puzzle_evidence(Pub);
puzzle_evidence(Pub) when is_binary(Pub), byte_size(Pub) =:= 32 ->
crypto:hash(sha256, Pub).
%% @doc Puzzle validity against the application-configured difficulty.
-spec puzzle_valid(pubkey() | key_pair()) -> boolean().
puzzle_valid(X) ->
puzzle_valid(X, default_difficulty()).
-spec puzzle_valid(pubkey() | key_pair(), non_neg_integer()) -> boolean().
puzzle_valid(X, Difficulty) when is_integer(Difficulty), Difficulty >= 0 ->
has_leading_zero_bits(puzzle_evidence(X), Difficulty).
%%------------------------------------------------------------------
%% Internals
%%------------------------------------------------------------------
-spec default_difficulty() -> non_neg_integer().
default_difficulty() ->
application:get_env(macula_identity, puzzle_difficulty, ?DEFAULT_PUZZLE_DIFFICULTY).
-spec grind(non_neg_integer()) -> key_pair().
grind(Difficulty) ->
Kp = generate(),
grind_loop(Kp, Difficulty, puzzle_valid(Kp, Difficulty)).
-spec grind_loop(key_pair(), non_neg_integer(), boolean()) -> key_pair().
grind_loop(Kp, _Difficulty, true) ->
Kp;
grind_loop(_Kp, Difficulty, false) ->
grind(Difficulty).
-spec has_leading_zero_bits(binary(), non_neg_integer()) -> boolean().
has_leading_zero_bits(_Bin, 0) ->
true;
has_leading_zero_bits(Bin, N)
when is_integer(N), N > 0, N =< bit_size(Bin) ->
<<Prefix:N, _/bitstring>> = Bin,
Prefix =:= 0;
has_leading_zero_bits(_Bin, _N) ->
false.