Skip to content

docs(clearsign): roadmap to a KeepKey-held signing authority - #369

Open
BitHighlander wants to merge 9 commits into
developfrom
docs/clearsign-delegation-roadmap
Open

docs(clearsign): roadmap to a KeepKey-held signing authority#369
BitHighlander wants to merge 9 commits into
developfrom
docs/clearsign-delegation-roadmap

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

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.h already 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-after fields 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.

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.
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant