macula_hyparview_endorsement (macula v11.4.0)

View Source

Realm-join handshake helpers (Phase 5.6).

Building block for the admission flow of a new station into a realm. The station presents a realm_member_endorsement record signed with the realm key, and the receiving peers verify it against the realm key id they trust for that realm before admitting the station into the HyParView active or passive view.

This module is pure: it does not talk to the network. Callers (typically the per-station dispatcher) feed the endorsement, as its wire form as received, into verify_endorsement/3 and act on the outcome. A member's endorsement slot is shared among signers, so slot_endorsement/3,4 reads it from the entries a lookup of that slot returns and takes only the realm key's entry: an endorsement or a tombstone under any other key changes nothing.

Acceptance rules

  • The record verifies under the verifier's profile (macula_record:verify/2): its carried key, its signature, and its created_at and expires_at.
  • Its type is the realm member endorsement (0x05).
  • Its signer's key id is the realm key id the node trusts for the realm.
  • The payload realm_id equals the expected realm id.
  • The payload member_node equals the node_id the joining peer claims, so no peer can present another member's endorsement.
  • valid_from is at most now and valid_until at least now: the endorsement is active.
  • valid_until is not before valid_from, and at most 30 days after it (macula_record:max_endorsement_window_ms/0).

Reference: plans/PLAN_MACULA_V2_PART6_PROTOCOL.md §9.6.

Summary

Functions

Build the JOIN frame a joining station sends to one of the realm's known stations, carrying its endorsement as the record's wire form. The frame carries no signature of its own: neighbour signatures belong to the connection (D17).

slot_endorsement/4 at the current time.

The endorsement a member's slot holds at the time Now, from the entries a lookup of that slot returns, as wire forms or {key, tbs, signature} maps. Only the entry under the realm key Trust pins counts (macula_record:signer_entry/4), so an endorsement or a tombstone under any other key changes nothing and costs no verify. The realm key's endorsement admits Member as verify_endorsement/3 checks it. The realm key's tombstone of this member's endorsement gives withdrawn while the tombstone lives, which lasts past the endorsement's own expiry. A realm key tombstone of another member's endorsement is refused as wrong_member, of another realm's as wrong_realm, and of another type as wrong_type. The stats are signer_entry/4's.

Verify that an endorsement, as its wire form or its {key, tbs, signature} map, admits Member to the realm that Trust names. Returns {ok, Roles} with the endorsed roles, or {error, Reason}; callers treat any error as a refusal and drop the pending join.

Types

node_id/0

-type node_id() :: <<_:256>>.

realm/0

-type realm() :: <<_:256>>.

slot_error/0

-type slot_error() :: not_found | withdrawn | verify_error().

trust/0

-type trust() ::
          #{profile := macula_crypto_profile:profile(), realm := realm(), realm_key_id := <<_:256>>}.

verify_error/0

-type verify_error() ::
          record_too_large | malformed | signature_invalid | alg_mismatch | not_yet_valid | expired |
          key_id_mismatch | wrong_type | untrusted_signer | wrong_realm | wrong_member |
          endorsement_expired | endorsement_window_too_long | endorsement_window_reversed.

Functions

build_join(Realm, NewMember, Endorsement)

-spec build_join(realm(), node_id(), binary()) -> macula_frame:frame().

Build the JOIN frame a joining station sends to one of the realm's known stations, carrying its endorsement as the record's wire form. The frame carries no signature of its own: neighbour signatures belong to the connection (D17).

slot_endorsement(Entries, Trust, Member)

-spec slot_endorsement([binary() | map()], trust(), node_id()) ->
                          {{ok, [binary()]} | {error, slot_error()}, macula_record:signer_entry_stats()}.

slot_endorsement/4 at the current time.

slot_endorsement(Entries, Trust, Member, Now)

-spec slot_endorsement([binary() | map()], trust(), node_id(), integer()) ->
                          {{ok, [binary()]} | {error, slot_error()}, macula_record:signer_entry_stats()}.

The endorsement a member's slot holds at the time Now, from the entries a lookup of that slot returns, as wire forms or {key, tbs, signature} maps. Only the entry under the realm key Trust pins counts (macula_record:signer_entry/4), so an endorsement or a tombstone under any other key changes nothing and costs no verify. The realm key's endorsement admits Member as verify_endorsement/3 checks it. The realm key's tombstone of this member's endorsement gives withdrawn while the tombstone lives, which lasts past the endorsement's own expiry. A realm key tombstone of another member's endorsement is refused as wrong_member, of another realm's as wrong_realm, and of another type as wrong_type. The stats are signer_entry/4's.

verify_endorsement(Signed, Trust, Member)

-spec verify_endorsement(binary() | map(), trust(), node_id()) ->
                            {ok, [binary()]} | {error, verify_error()}.

Verify that an endorsement, as its wire form or its {key, tbs, signature} map, admits Member to the realm that Trust names. Returns {ok, Roles} with the endorsed roles, or {error, Reason}; callers treat any error as a refusal and drop the pending join.