RFC 0037 — ACE expiry is judged against the application’s now¶
Note
Status: accepted. This page is an accepted change proposal, kept as the record of why the specification reads as it does. RFCs are proposals and history, not the standard. The normative specification is Protocol v1 and the annexes its §3 incorporates; where an RFC and the specification differ, the specification wins. All RFCs, with their status, are listed in the ADR and RFC index.
Field |
Value |
|---|---|
RFC |
0037 |
Title |
ACE expiry is judged against the application’s |
Status |
accepted (2026-10-25). The maintainer ruled the direction on 2026-10-24 (Q2: “ACE expiry takes the app’s |
Author(s) |
AvatarSD (maintainer) |
Created |
2026-10-25 |
Comment window |
Waived by default while the project is solo-maintained (GOVERNANCE.md §”Errata, amendments, and the comment window”). Invoke it explicitly if outside input is wanted. |
Instrument |
Amendment. reference/05 § |
Tracking issue |
|
Target spec version |
v1 itself. |
Amends |
reference/05 § |
1. Summary¶
An ACE may carry expires_ns, an absolute instant after which it no longer applies. Until now
the reference node compared it against its own system clock, read inside the ACL gate. That
breaks the standing rule that the library reads no clock, and it leaves the spec silent on which
clock a lease is judged by.
This RFC makes the reference clock the application’s:
expires_nsis nanoseconds on the clock of the node’s application. The epoch is the application’s choice and must match the clock it writesexpires_nsagainst.At check time the node compares it against the
nowthe application supplies. An ACE is non-expired iffexpires_nsis absent or0, ornow < expires_ns.No
now, no lease (fail closed). A node whose application supplies nonowcannot prove a lease live or lapsed. An ALLOW ACE with a non-zeroexpires_nsthen grants nothing, and a DENY ACE with one still refuses. A standing ACE (expires_nsabsent or0) is unaffected.The node reads no clock to judge expiry.
2. Motivation¶
The library reads no clock. A deadline is driven by the application through a seam that takes
nowfrom the application’s own clock (the standing design rule). The ACL gate was one of the remaining clock reads; #1665 and #1685 had already narrowed it to the leased case.A node without a wall clock. An MCU often has no calibrated wall clock at boot, so the system clock reads the epoch, or something close to it. Every lease then looked live for decades. Only the application knows whether its clock is trustworthy yet.
One clock per deployment. The owner writes
:acl, so the owner already chooses the clockexpires_nsis written against. Judging it against the same clock is the only reading under which a lease means what its writer meant.
3. Specification¶
3.1 reference/05 §0x0A¶
The ACE layout line for expires_ns gains its unit:
NAME "expires_ns" VALUE <u64> ; optional; ns on the application's clock, 0 = never
and a paragraph follows the enforcement paragraph:
Expiry is judged against the application’s
now(RFC-0037). An ACE’sexpires_nsis an absolute instant in nanoseconds on the clock of the node’s application; the epoch is the application’s choice and MUST match the clock that writesexpires_ns. At check time the node compares it against thenowits application supplies: the ACE is non-expired iffexpires_nsis absent or0, ornow < expires_ns. A node whose application supplies nonowMUST fail every lease closed: an ALLOW ACE with a non-zeroexpires_nsgrants nothing, and a DENY ACE with one still refuses. An ACE withexpires_nsabsent or0is unaffected. An expired ACE still closes an otherwise-open vertex, as before. No wire byte changes.
3.2 Why a leased DENY still refuses without a now¶
Under the full policy (ordered, first match per bit) a list may put a leased DENY in front of a
standing ALLOW for the same subject. Skipping the DENY for want of a clock would grant what a
live clock refuses. So without a now a lease is treated as unprovable in the direction that
refuses: an ALLOW does not grant and a DENY still applies. The ALLOW-only MCU profile stores no
DENY, so there it reduces to “a leased grant gives nothing”.
3.3 What does not change¶
The wire.
expires_nskeeps its u64 layout, its optionality and its strict parsing (an empty or over-wide payload is still refused, so an absent value never reads as “never expires”).Expiry is
expires_ns <= now, not<: at the deadline the grant is already gone.Only the merge is cached, never a verdict (ADR-0050), so expiry still needs no invalidation.
4. Reference implementation¶
tr::graph::subject_lookup_fn_tgains a last parameter,std::uint64_t& now_ns. The gate sets it totr::graph::kAceNowUnknownbefore the call, and the application overwrites it with itsnow. The ADR-0047{fn, ctx}shape stays, and nothing is allocated.graph_t::acl_allowsuses that value only when the effective ACL it evaluates holds a lease.now_ns()and itssystem_clock::now()are deleted fromgraph.cpp.detail_acl::ace_appliesimplements §3.2: atkAceNowUnknowna lease reads as lapsed, except that a leased DENY still applies.The public API break is recorded in
core/CHANGELOG.md.
Pinned by core/tests/acl_clock_read_test.cpp. A fake application clock drives a lease through
the real hook. The test also covers the no-now rule on the graph and on the full policy, and
it counts zero system_clock::now() calls across every gate.
5. Conformance¶
No conformance vector changes. The acl/acl-aces vector pins the ACE wire shape, which is
unchanged, and no vector evaluates a lease against a clock.