macula_ucan (macula v13.3.0)
View SourceUCAN tokens in the node's crypto profile (plan decision D7), and a provider's authorization of one (D7 check 2).
A token is a JWT, header.payload.signature, each part base64url without padding. The header names the profile's algorithm: RFC 9964's ML-DSA-87 in pq_pure, and ML-DSA-87-PS384, the IETF LAMPS composite id-MLDSA87-RSA4096-PSS-SHA512, in pq_hybrid (JOSE has no name for it). The signature is the issuer's node key's signature over the header and payload as they are sent, made by macula_node_keys:sign/2, so ML-DSA-87 is macula-mldsa and the composite's RSA-PSS half is OTP's.
The payload's iss is a did:key for the issuer's key as carried (D13): multibase base58btc over the varint of a multicodec and the key. pq_pure uses mldsa-87-pub, 0x1212; pq_hybrid has no multicodec yet and uses Macula's own key type, 0x300087, from the private-use range. aud is the audience's node_id in lowercase hex, since a token is presented by the node it names, inside a request that node signed (D7 check 2). Every token has an exp, in seconds.
authorize/3 is the provider's check, after the request's own signature and target have verified: the header and payload are verified over the bytes as received, never as re-encoded JSON, then the issuer, the audience, the validity window and the capability.
A token may be one the caller was delegated. Its prf names its parent by proof_id/1, the lowercase hex of the SHA-384 of that parent's bytes as they travel, and the parents travel beside it in the request's caller-signed proofs set, which reaches this module as the context's proofs map. The chain is walked to its root, which must be the issuer the policy names, and every step is checked: the parent's audience is the node_id of the child's issuer key, each link's own signature and validity window hold, can is equal at every step, and a child's capability is covered by one of its parent's (covers/2, D7's narrowing matrix). A token names at most one parent, and every proof that travelled must be referenced, so an unused one is refused rather than ignored.
A capability's with is an MRI: mri:realm:<realm>, mri:org:<realm>/<org> or mri:proc:<realm>/<procedure>, where a procedure's org is the text before its first /. A realm id is SHA-256 over the normalised realm name (D7), so the grant's name is checked against the realm id the request carries, with nothing looked up.
Summary
Types
The issuer a policy requires: a node's node_id for ucan_required, a realm key's key id for realm_member_required.
What a capability grants, parsed from its with: a realm, an org of that realm, or one procedure of that realm.
Functions
Whether Token authorizes the verified caller under Policy, in the provider's profile at Now in seconds: its claims when it does, or the first reason it does not. Never raises on the token.
The key a did:key carries, when it is a key in its one carried form for the profile (D13).
Whether a grant covers another grant or a request, by D7's narrowing matrix: a realm grant covers its realm, an org grant covers that org and its procedures, and a procedure grant covers only itself. False for a grant that is not an MRI of the three forms, whose realm name is not canonical, or whose procedure has no org namespace.
A token from the issuer's key, a key of a purpose that signs with the profile's identity algorithms, for the audience's node_id, granting Capabilities until exp. nbf, nnc, fct and prf are optional.
The did:key for a key as carried in a profile.
The id a child's prf names a parent token by: the lowercase hex of the SHA-384 of that token's bytes as they travel, the three base64url parts and their dots. A token re-encoded on the way has another id (D7, D24).
Types
The issuer a policy requires: a node's node_id for ucan_required, a realm key's key id for realm_member_required.
-type issuer_id() :: <<_:256>>.
-type refusal() ::
malformed | wrong_algorithm | signature_invalid | not_the_issuer | not_the_audience |
expired | not_yet_valid | missing_capability | missing_proof | unreferenced_proof |
not_the_delegate | chain_not_linear | grants_more_than_proof | can_changed | wrong_realm |
realm_name_not_canonical | procedure_without_org.
What a capability grants, parsed from its with: a realm, an org of that realm, or one procedure of that realm.
Functions
-spec authorize(term(), policy(), #{caller := macula_node_keys:node_id(), profile := macula_crypto_profile:profile(), now := integer(), realm => <<_:256>>, procedure => binary(), proofs => #{binary() => binary()}}) -> {ok, map()} | {error, refusal()}.
Whether Token authorizes the verified caller under Policy, in the provider's profile at Now in seconds: its claims when it does, or the first reason it does not. Never raises on the token.
The context names the caller, the profile and the time, and, for a request, the realm id and procedure it is for and the proofs that travelled with it. A context with no realm and procedure checks the token alone, as a gate that has no request in front of it.
-spec carried_key(binary(), macula_crypto_profile:profile()) -> {ok, binary()} | error.
The key a did:key carries, when it is a key in its one carried form for the profile (D13).
Whether a grant covers another grant or a request, by D7's narrowing matrix: a realm grant covers its realm, an org grant covers that org and its procedures, and a procedure grant covers only itself. False for a grant that is not an MRI of the three forms, whose realm name is not canonical, or whose procedure has no org namespace.
-spec create(macula_node_keys:node_key(), macula_node_keys:node_id(), [capability()], #{exp := non_neg_integer(), nbf => non_neg_integer(), nnc => binary(), fct => map(), prf => [binary()]}) -> binary().
A token from the issuer's key, a key of a purpose that signs with the profile's identity algorithms, for the audience's node_id, granting Capabilities until exp. nbf, nnc, fct and prf are optional.
-spec did_key(binary(), macula_crypto_profile:profile()) -> binary().
The did:key for a key as carried in a profile.
The id a child's prf names a parent token by: the lowercase hex of the SHA-384 of that token's bytes as they travel, the three base64url parts and their dots. A token re-encoded on the way has another id (D7, D24).