docs(clearsign): roadmap to a KeepKey-held signing authority - #369
Open
BitHighlander wants to merge 9 commits into
Open
docs(clearsign): roadmap to a KeepKey-held signing authority#369BitHighlander wants to merge 9 commits into
BitHighlander wants to merge 9 commits into
Conversation
Records why per-transaction clear-signing forces a delegation system, and what has to exist before one can ship. Written for audit. The argument, in short: The device cannot decode an arbitrary contract call by itself, so something must tell it how to render one, and it must be able to verify who said so. v2 schema blobs solve this with no hot key at all -- signed once offline, device-decoded from the calldata it is about to sign. But v2 cannot reach uncatalogued contracts, values not derivable from calldata, aggregator routes, or fresh reputational context. Those need a signature over THIS transaction, produced NOW, which forces an online key. An online key will eventually be compromised. If it is the anchor compiled into firmware, an attacker can render any drainer as a benign transfer on the trusted display -- the exact attack clear-signing exists to prevent, with user confidence raised rather than lowered -- across every device ever shipped, fixable only by a firmware release. So the root signs delegations and nothing else, while a scoped, short-lived, revocable delegate signs payloads. The root belongs on a KeepKey rather than a conventional HSM: the attestor already forces a human to read program ID, discriminator and every argument declaration on a trusted screen before a signature exists. For an authority that certifies what a transaction means, that is the product, not overhead. The hard part, stated plainly for auditors: a KeepKey has no clock and no network, so it cannot enforce "valid one month" or "revoked" -- only ORDERING, via a monotonic epoch. A device that never sees a newer epoch keeps accepting an old delegation indefinitely. The one-month bound is real only for a device that connects. That epoch cannot live in the public storage section, which has no authenticated integrity against physical flash modification (the reason RC18 rejected persistent trust anchors): an attacker who lowers the stored minimum re-enables every revoked delegate. It is the same primitive the anti-rollback RFC needs, and must not be built twice -- the weaker of two ratchets would set the security level. Consequence worth surfacing: the security epoch is now on the critical path for both PIN-KDF hardening and delegated clear-signing. Phase 0 is what the next release ships -- advanced-flag-only, RAM-only signers, annotation-only metadata with the raw review retained -- and each later phase lists what an auditor should attack.
Answers the open question directly: an offline device cannot be made to honour a revocation. Revocation needs the device to either LEARN it is revoked -- over a channel the adversary controls -- or expire on its own, which needs a clock it does not have. A withheld message is indistinguishable from no network, so CRLs, on-chain revocation records and broadcast kill messages are all unenforceable against the party we already assume is hostile. Web PKI settled this when CRLs failed and OCSP stapling won. Same answer here: stop revoking, start expiring. Credentials are short-lived, revoking means not reissuing, and the device demands proof of freshness -- a positive statement it can check -- rather than proof of absence, which it cannot. Bitcoin headers are a good freshness oracle. Validation is one sha256d and a target compare, and forging a header at mainnet difficulty means outspending the network. Crucially it beats a KeepKey-signed epoch bump on liveness: any host, explorer or full node can advance the tip, whereas a KeepKey-signed epoch only advances if the device reaches KeepKey infrastructure -- exactly the channel an attacker suppresses. It also gives elapsed-time semantics rather than bare ordering (one month is about 4320 blocks). It does not remove the residual. Withholding headers still pins a device in the past, and the accepted tip still needs integrity-protected monotonic storage or flash modification resurrects every expired delegate. Track cumulative work rather than height: difficulty can legitimately fall 4x per retarget, so a hardcoded target is soft and a low-difficulty fork is the attack. Freshness must be signed by the ROOT. A delegate signing its own freshness proof is theatre, since a stolen delegate signs one too. That collapses the design to: the root reissues monthly, each certificate names a Bitcoin block, the device accepts it only within ~4320 blocks of its accepted tip. No revocation channel gets built because none would work. Threat model recorded honestly: server compromise with an honest host is bounded and defended; server compromise plus a hostile host is not defended, though the blast radius stays one delegate's scope because forging a new delegate still needs the root; a device that never connects is frozen and indefinitely trusting, which is unavoidable for any offline device. On-chain revocation via OP_RETURN and an SPV proof is rejected: it is a negative statement again, so the attacker just withholds the proof.
Three corrections after reading Ledger's Ethereum app and Secure SDK. 1. The previous text said Ledger "reached the same conclusion" about short-lived revocable delegates. That is not substantiable from public code and is exactly the kind of claim an auditor disproves before distrusting everything around it. In the open Speculos implementation the certificate's TIME_VALIDITY is only checked structurally for length, not against a trusted clock, and VALIDITY_INDEX is not visibly ratcheted against tamper-resistant monotonic state. Speculos emulates BOLOS rather than being the complete proprietary OS, so the defensible statement is not "Ledger has no revocation" but "the public device and app code does not demonstrate an offline expiry/revocation solution we can copy". What IS substantiable is worth copying rather than reinventing: an OS-level PKI with a root CA, runtime certificate loading and chaining, and -- the important part -- CAPABILITY-SCOPED certificates. The Ethereum app does not trust a bare Ledger-approved key; it requires a certificate whose usage is TX_SIMU_SIGNER. Certificates now carry usage, chain scope and a no-re-delegation flag, so a compromised TOKEN_METADATA signer can never author a dynamic transaction interpretation. Ledger's transaction binding is adopted verbatim and made mandatory: report.tx_hash against the tx being signed, report.from against the DEVICE-derived sender rather than the host-claimed one, report.chain_id, and the EIP-712 domain hash. Without these, malware attaches a benign report for transaction A to malicious transaction B. 2. Bitcoin is now an input to the single security epoch rather than a parallel mechanism -- the previous draft argued against two ratchets and then proposed one. With a constraint that falls out of unifying them: an epoch advance derived from Bitcoin must never trigger a destructive action. Expiry is fail-safe and may key off it freely; anything that migrates or erases storage must require a root-signed statement, or forged headers stop being a nuisance and become a way to trigger migrations. 3. The argument for Bitcoin over a root-signed epoch broadcast is corrected. It is not suppression-resistance (a hostile host suppresses either equally) and barely ceremony cost (monthly delegate reissuance implies a monthly ceremony anyway). It is VENDOR INDEPENDENCE: under a broadcast model, freshness depends on KeepKey continuing to publish, so if the company is acquired or goes dark every outstanding delegate stays valid forever. With Bitcoin they expire regardless. Also records an asymmetry that materially lowers the cost: the attacker wants the tip LOW, so a forged high-work chain expires their own stolen delegate. Forgery gains them nothing here, unlike an SPV payment client where it gains money. A firmware-carried checkpoint plus a bounded window may therefore suffice instead of full consensus validation -- valid only while epoch advance stays non-destructive, per constraint 2.
…hness Adopts the review. Four substantive changes. 1. Unify the ratchet SUBSTRATE, not the epoch value. One integrity-protected monotonic state facility -- one implementation, one atomic update path, one anti-rollback guarantee, one audit surface -- holding domain-separated counters (firmware_epoch, storage_epoch, clearsign_epoch, clearsign_freshness), with different authorities permitted to advance different fields. Root signatures may advance the first three; Bitcoin work may advance only clearsign_freshness. Bitcoin-derived state can therefore never cause a KDF migration, storage rewrite, seed wipe, firmware trust change or PIN behaviour change regardless of how far it is pushed or how wrong the validator is. The previous "unify the epoch, separate the actions" relied on discipline at every call site; this is structural. 2. The delegate certificate carries its own Bitcoin anchor, so the trusted point is re-established at every issuance rather than going stale between firmware releases. The device validates only prev_hash linkage, that each header's hash meets its own claimed nBits target, sane target bounds, count, and accumulated work. That is sound without consensus rules because claimed work is always backed by demonstrated work: cheap nBits yields trivial work and fails the threshold, hard nBits requires genuinely finding the hashes, and anchor linkage prevents splicing real headers from elsewhere. No retarget validation, no median-time-past, no version bits, no chain selection. The question is not "is this Bitcoin's canonical chain" but "does a chain descending from my trusted anchor contain enough genuine work". 3. Corrects "forged headers only help us". They cannot extend an attacker's authorization, but fabricated far-future freshness is monotonic and would make legitimate certificates read as ancient for years -- a permanent clear-sign outage. The consequence is bounded to denial of service PROVIDED Bitcoin-derived state has no authority over destructive operations, which change 1 now guarantees structurally. 4. Adds downgrade-by-expiry, which neither version had. Reducing authority is not automatically safe: if expiry drops the user into a flow that looks normal -- raw hex approved out of habit -- then forcing expiry downgrades protection rather than denying service. Expiry must fail closed and visibly, and must never resemble a successful signing flow. Phase 0's annotation-only shape with the raw review retained is already correct and must survive into later phases. Also records the governing invariant (Bitcoin evidence may only REDUCE clear-signing authority, never grant it or trigger destructive transitions), the two complementary revocation paths, the precise non-overclaiming Ledger wording, and three implementation constraints to price early: streaming O(1) header validation because 4320 headers is ~346 KB and cannot be buffered; anchor recency at issuance; and the direction of hashrate-drift error under AND-semantics.
…bearing Bitcoin-only firmware does not need clear-signing. A Bitcoin transaction is inputs, outputs, addresses and amounts, all rendered natively and completely; there is no opaque calldata for a signing authority to attest to. This is already true in the build rather than a change: signed_metadata.c sits inside if(NOT KK_BITCOIN_ONLY), and the attestor handlers are behind #if !BITCOIN_ONLY in fsm.c. Worth recording because the domain-separated ratchet design is what makes the exclusion possible. The substrate ships in both variants -- firmware_epoch and storage_epoch apply to bitcoin-only too -- but only clearsign_freshness and the header validator are full-variant. Under a single global epoch advanced by Bitcoin work, bitcoin-only would have to carry header validation just to stay coherent with an epoch it shares with storage anti-rollback. With domain separation the variant has no field Bitcoin has authority over, so it has no reason to validate a header at all. Two consequences that are easy to get backwards. The Bitcoin header validator ships in the MULTI-CHAIN firmware and not in the bitcoin-only firmware, which reads as inverted and will need explaining more than once. And excluding bitcoin-only does not split the ROM budget, it concentrates all of it on the full variant -- the build that was down to ~572 bytes free before the 34 KB reclaim in #339 and #340. Bitcoin-only has headroom precisely because the coins are stripped, and none of that headroom helps here. The open cost question is corrected accordingly. Revisit only if bitcoin-only ever wants a capability needing external attestation; TRUSTED_NAME for Bitcoin addresses is the plausible one. Not a requirement today, not to be built speculatively -- recorded so the exclusion stays a decision rather than becoming an oversight.
Closes the bypass that mattered most. Downgrade-by-expiry had a companion that is EASIER to exploit: an attacker with an expired delegate need not submit the expired certificate at all -- they omit the metadata entirely. No certificate, no descriptor, no validation failure, and the device takes the ordinary blind path. So this cannot be "if metadata is present, validate it"; it needs a device-enforced signing policy under which missing, malformed, invalid, expired, wrongly scoped and tx-mismatched metadata are all the same terminal condition, with no signature produced. Raw review is additive on a valid path, never the automatic fallback from a failed one. Two distinct protections, and Phase 0 establishes only the first: successful clear-signing cannot SUPPRESS the raw review, and failed clear-signing cannot FALL THROUGH to an otherwise normal flow. No inline "Continue anyway" -- the device returns a failure and a determined user must leave the flow, enable the policy and start again, or the error becomes one more click-through warning. Four additions to the policy model: - The policy is device state and the host must never select it. If a signing request can name its own policy, the host always names blind-signing and the mechanism evaporates. The selection channel is itself a trust boundary. - The policy is per transaction class, not global. A plain transfer, or an ERC-20 transfer the device decodes with its built-in table, has nothing to attest; requiring attestation there blocks ordinary sends. The device must classify what it can render itself -- from the transaction, never from a host hint -- and the policy governs only the residue. - Rollout ordering: a policy users are forced to disable is worse than no policy. Defaulting to required before catalog coverage is high makes the support answer "turn on blind signing", which users then leave on forever. - Open question flagged rather than decided: whether blind-sign enablement should be sticky. Permanent enablement means the first support incident disables the protection for good, which is how security settings decay. Wording corrected throughout: revocation cannot be FORCED onto an offline device, and freshness-gated expiry becomes enforceable only once sufficient authenticated evidence of progress is received. Emergency epoch advance is "invalidation on receipt", not unconditional revocation. Dropped the CRL/OCSP historical aside, which invited an argument irrelevant to the cryptography. Validator engineering recorded: 5-10 KB ROM estimate by component against ~35 KB headroom, hard criteria of <=512 B validator state, <=256 B added stack frame and zero whole-proof buffering, a Begin/Chunk/Finish session that folds incrementally and advances the ratchet only atomically at Finish, and the insight that accumulated work need never be persisted -- it is a witness authorising a height advance, and monotonic height alone evaluates any certificate's expiry, which also sidesteps comparing work across different anchors. Plus nBits caching with its bounded compute-DoS, the compact-target rejection list, a required endian golden test, and the target-floor fallback with its worse hashrate-collapse behaviour. Bitcoin-only stays excluded on principle, not on budget: memory availability is not an inclusion criterion on a trust boundary.
BitHighlander
force-pushed
the
docs/clearsign-delegation-roadmap
branch
from
August 11, 2026 06:51
676c3a1 to
97617a1
Compare
…st one The roadmap read as a settled architecture with open parameters. It is not: seven of its foundations are undecided, six of them change what a ROM measurement would even be measuring, and one -- custody -- is not decidable in a document at all. A new §0 states that up front, with an ordering, because the expensive mistake here is costing a validator against a substrate nobody has chosen. Resolved in the text: #8 One canonical certificate. §5b and §6 described the same signed object with different fields, and the acceptance rule then tested "epoch within [epoch_min, epoch_max]" against an `epoch` field that appeared in neither. Three descriptions of one signed object is three implementations of one trust boundary. `bitcoin_not_before/not_after` are gone (anchor + delta said it already, in a form that invites reading it as a clock); the range is gone (a certificate has one epoch). Phase 1's "built-in anchor" and Phase 2's "pin the root" stop contradicting each other once they are named as what they are: a SCHEMA root and a DELEGATION root, separate keys with separate ceremonies -- which also lets Phase 1 proceed while custody is open. #1 "clearsign_freshness absent or inert" in bitcoin-only meant full -> bitcoin-only -> full resurrected every expired delegate, and contradicted the shared substrate two paragraphs above it. Now: carried opaquely, unable to advance or lower, with the alternative (full firmware refusing to clear-sign when freshness cannot be established) stated explicitly, because the default behaviour of a missing field is to read as zero and accept everything. #6 BitcoinFreshnessBegin no longer carries the anchor or the thresholds. A host that supplies the values deciding whether its own proof passes picks an anchor it can cheaply extend; it may reference a certificate the device has already verified, and stream headers. Plus rate-limited advances -- an attacker needs no forgery, only genuine chains replayed ever longer, one flash commit each. Specified, with the remaining choice named: #2 The four-field SecurityRatchets facility does not exist. The anti-rollback RFC reserves ONE OTP block as a 256-step unary counter, which cannot hold a block height, and authenticated flash stops forgery but not restoration of an authenticated older snapshot. Required shape: OTP-backed coarse generation plus an authenticated journal bound to that generation -- with rollback tolerance, checkpoint granularity, wear budget and power-loss state machine as four decisions, not details. #3 "ROOT SIGNATURE -> firmware_epoch, storage_epoch, clearsign_epoch" reads as one key over all three, reintroducing one level up the concentration §3 rejects. Named roots per ratchet, cryptographic separation, and cross-protocol replay as a release gate. #4 The RFC requires rejecting a low-epoch candidate BEFORE erasing, and old-or-new bootability after power loss. handler_erase() in tools/bootloader/usb_flash.c:425-493 erases sectors 7-11 on FirmwareErase, before any candidate bytes exist, with one application slot. Three ways out, one of which is to admit the recovery-only state in writing -- today's honest description, and still a choice that has to be made rather than inherited. #7 The blind-sign policy is security-critical persistent state with no integrity protection specified. In today's public storage section a physical attacker flips one bit and enables the downgrade policy directly, silently, without touching a certificate. Authenticated storage or a session-scoped model, plus stated behaviour after reset, after a variant change, and on corruption -- fail closed each time. Left for an owner: #5 Single-root custody. One dice-generated key on one KeepKey, holding the one privilege §6 grants -- warning-free rendering. N-of-M across independent devices and locations, with rotation, backup, disaster recovery and an overlapping-anchor transition. Phase 2 is gated on it: a root in shipped firmware cannot be un-shipped. §9 keeps the genuine parameters and says plainly that they sit downstream, and Phase 3's release gates gain one negative test per blocker.
…s proposed Two corrections from review. **Schema-root custody.** The previous revision said Phase 1 could ship on "a lighter custody model" because the schema root grants no per-transaction authority. That reasons about the key's scope and forgets its remedy. §6 grants a root-verified chain exactly one privilege -- warning-free rendering -- so a compromised schema root puts a false interpretation on the trusted display with the warning suppressed, which is the same outcome as for the delegation root. In one respect it is worse. A delegate certificate carries cert_epoch, an anchor height and an expiry delta, so a stolen delegate ages out on its own; that is the entire point of §5b. The v2 catalog has none of those, so a compromised schema root has no expiry and no ratchet, and its only remedy is a firmware release every user must choose to install. Both roots therefore need threshold custody. Parameters may differ -- catalog reissuance is batched, delegate issuance is monthly -- but "no single device or person can produce this signature" does not. Phase 1 is now gated on the schema root's custody decision and Phase 2 on the delegation root's. Splitting the roots buys cryptographic separation and independent compromise stories; it does not buy a phase that skips custody. §0 also notes that custody is last in the suggested order yet gates the earliest ship, so it should start in parallel. **Blocker 8 was specified, not resolved.** The field table removed the three-way inconsistency but left every offset after 0x02 as '*' -- no widths, no endianness, no chain_scope bounds or ordering, no signature algorithm, no signed transcript length, and no definition of the certificate hash that BitcoinFreshnessBegin references. Calling that "canonical" repeated the overclaim the section was fixing. There is now a concrete layout with byte offsets, using only primitives the device already commits to in signed_metadata.h (33-byte compressed secp256k1, 64-byte compact ECDSA over sha256): the signed transcript is exactly bytes 0..176, the canonical certificate hash is sha256 over all 241 bytes including the signature so the two can never be confused, and unknown versions, bad chain_count, non-ascending or duplicate chain ids, non-zero padding and wrong total length are refusals rather than fields to skip. Marked **proposed, not ratified**, in the text and in §0's status column, which now also says what Resolved and Specified mean so they stop being read as the same word.
BitHighlander
added a commit
that referenced
this pull request
Aug 12, 2026
…ings The previous revision said all four code findings were closed. Two were not, and the head hashes for #366 and #368 were a commit behind. Both reopened findings were the same mistake wearing different clothes -- a fix correct as far as it went, summarised as if it went further. #366 gated a list of call sites and called it "every key-material draw", missing the Orchard RedPallas nonce in a submodule nobody re-audits. #369 removed the certificate's three-way field inconsistency and called the result "canonical" while every offset after 0x02 was still a '*'. Neither was a coding error; both were scope claims outrunning the work, and both were found by reading the claim against the code rather than against the diff. That is now stated at the top, because it is the most useful thing in this document for whoever reads it next. Also recorded: the #367 self-test passed throughout the period its gate was inoperative, because it exercised the one input shape in which the bug is invisible. A green check on a gate is evidence about the test, not the gate. Heads, statuses, the #369 blocker table (8 downgraded to proposed, 5 widened to gate both phases) and the test counts are updated to match.
…unting **BLOCKER 9 — nothing bootstraps.** The proof session required "a certificate the device has already verified", while §6 acceptance requires satisfied freshness. A factory-fresh device, or one returning from bitcoin-only with no established freshness, has no certificate that passes acceptance, so it can never start the session that would establish the freshness acceptance demands. Split the word: AUTHENTICATED is encoding + root signature + device/domain bindings; AUTHORIZED is that plus epoch, freshness, usage/scope and transaction binding. Freshness proofs may reference an authenticated certificate, which is sound because every value a proof depends on is already covered by the root signature -- authentication alone fixes the constraints the host would otherwise choose. Warning-free rendering requires authorized. Note this loosens nothing: an authenticated-only certificate can drive the ratchet forward, and forward only ever REDUCES authority. **BLOCKER 10 — the global height throws the work witness away.** Each proof is checked against its own certificate's expiry_min_work, but what is committed is one global height that then evaluates every certificate. So a proof under a weak certificate advances the height far enough to expire a certificate whose higher threshold nothing ever demonstrated, and whoever holds the weakest policy ever issued sets the real cost of advancing everyone's clock. Still fail-closed for authorization; what it destroys is the cost bound that made permanent clear-sign DoS expensive. Three options stated, not resolved, plus: cap the committed height at the work-qualified checkpoint rather than crediting every accepted header, because the headers past that point are free to produce. Smaller: the root signature must be low-S canonical, or one certificate has two valid encodings and therefore two cert_hash values -- and cert_hash covers the signature. §9 no longer claims certificate encoding is settled, which contradicted §6's own "proposed, not ratified". Phase 3's release gates gain one test per new blocker.
BitHighlander
added a commit
that referenced
this pull request
Aug 12, 2026
…able Round-2 review reopened both code PRs. #368 failed both ARM builds on _Alignas and then failed catalog validation on a test name renamed in 280f3b6; #366 failed every link on random_uniform and layout_warning_static, and its continuous RCT/APT state was never fed by the default path. So the previous claim that #369 was the sole reason RC28 is not merge-ready was wrong, and both are now listed as reopened-and-fixed rather than closed. The header now carries the pattern instead of a fourth round of individual corrections, because it has repeated three times in different shapes: work correct as far as it goes, reported as if it went further. The instance worth acting on is the last one -- "verified locally" meant an emulator build, and an emulator build proves nothing about shipping firmware. _Alignas compiles under clang and is rejected by the ARM toolchain; a missing link edge only appears under the archive ordering the ARM target uses. Neither is visible from cmake -DKK_EMULATOR=ON. There is a mechanical fix rather than a resolution to try harder, so it is now in the header AND the build recipe: a four-minute Docker cross-compile in CI's own image, both variants, no toolchain install. Every ARM claim in this document was reproduced that way before being written down, including the ROM delta that corrects the earlier "no ROM" claim for the bootloader. Also records that #367's structural gate is not blocked on obtaining signing keys -- pubkeys.h already has all five; it needs a host-side secp256k1 verifier.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Docs only. Records why clear-signing needs a delegation system and what must exist before one can ship — the audit roadmap.
The argument
The device cannot decode an arbitrary contract call itself, so something must tell it how to render one, and it must verify who said so.
v2 schema blobs already solve this with no hot key — signed once offline, device-decoded from the calldata it is about to sign, so the display is bound to the signature by construction. But v2 cannot reach uncatalogued contracts (where drainers live), values not derivable from calldata, aggregator/bridge routes, or fresh reputational context. Those need a signature over this transaction produced now → an online key.
An online key will eventually be compromised. If it is the anchor compiled into firmware, an attacker can render any drainer as a benign transfer on the trusted display — the exact attack clear-signing exists to prevent, with user confidence raised — across every device ever shipped, fixable only by a firmware release.
So: the root signs delegations and nothing else; a scoped, short-lived, revocable delegate signs payloads. Same conclusion Ledger reached, same reason.
Why the root belongs on a KeepKey
fsm_msg_clearsign_attestor.halready forces a human to read the program ID, discriminator and every argument declaration on a trusted screen before a signature exists. A YubiHSM signs whatever the calling process hands it and the operator sees nothing. For an authority whose job is certifying what a transaction means, the signing device displaying the claim is the product.The hard part — stated for auditors, not buried
A KeepKey has no clock and no network. It cannot enforce "valid one month" or "revoked" — only ordering, via a monotonic epoch. Certificate
not-afterfields constrain the issuer's discipline, not the device's acceptance; host-supplied time is worthless against an attacker holding a stale delegate.Residual risk that must appear in any audit: a device that never sees a newer epoch keeps accepting an old delegation indefinitely. The one-month bound is real for a device that connects and unbounded for one that does not.
And the epoch cannot live in the public storage section — no authenticated integrity against physical flash modification, the same reason RC18 rejected persistent trust anchors. An attacker who lowers the stored minimum re-enables every revoked delegate.
Sequencing consequence
Delegation and anti-rollback are the same primitive. Building two ratchets means two failure modes and the weaker one sets the security level. So the security epoch is now on the critical path for both PIN-KDF hardening and delegated clear-signing — which argues for higher priority than a storage-only view suggests.
Phases
Phase 0 is what the next release ships — advanced-flag-only exactly as RC21 is coded, RAM-only signers, annotation-only metadata with the raw review retained. Each later phase (v2 catalog → built-in anchor → delegation) lists what an auditor should attack; the highest-value Phase 0 attack is a rogue signer suppressing the raw-data screen, the failure that closed fw #322.
Open questions are collected in §9 rather than decided here — epoch storage medium, certificate encoding (compact fixed layout, not X.509 on a trust boundary), delegate count/scoping, and whether a delegated render is truly warning-free.