Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/security/7.15.0-rc21-clearsign-release-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Firmware 7.15.0 RC21 ClearSign Release Control

Date: 2026-07-29

RC21 is the opt-in proving release for self-service ClearSign. It deliberately
does not pin a KeepKey production signer. The regular firmware contains the
constrained `KKSOLSC1` attestor, but its public-key and signing messages,
runtime signer loading, and runtime metadata verification are usable only while
the user has enabled `AdvancedMode`.

Loaded signer identities remain RAM-only. They are cleared by session teardown,
`ClearSession`, reboot, and disabling `AdvancedMode`. Metadata from a runtime
signer is annotation-only: EVM and Solana retain the baseline raw/unverified
review after the decoded screens.

## Human-attestation gate

The attestor must show every security-relevant declaration before it signs:

1. program and instruction labels;
2. the complete base58 program ID on its own confirmation;
3. the complete discriminator on its own confirmation;
4. every argument's ordinal, ABI type, and label; and
5. every displayed account index and label.

Program ID and discriminator may not share one standard notification. A
44-character base58 program ID consumes two body rows, and an 8-byte
discriminator cannot reliably fit in the remaining row. Argument types may not
be omitted: two different ordered type declarations can have the same total
width while assigning the same labels to different byte offsets.

Before RC21 is tagged, release evidence must include OLED captures from a
schema with a 44-character program ID, an 8-byte discriminator, all four
argument types (`u64 LE`, `u8`, `public key`, and `bytes32 hex`), a 16-character
argument label, and a 16-character account label. Every value must be complete
and readable. CI success alone does not prove this display boundary.

The candidate's framebuffer captures and exact input manifest are recorded in
[`evidence/7.15.0-rc21-clearsign-attestor`](evidence/7.15.0-rc21-clearsign-attestor/README.md).
Regenerate that evidence if any attestor parser or confirmation code changes.

## Host compatibility

The canonical RC21 protocol uses `SolanaSignTx` tags 9, 10, and 11 for
`schema_payload`, `schema_signature`, and `schema_signer_key_id`. Tags 5 through
8 are reserved for the transaction-bound `KKSOLSW1` descriptor and one-request
opaque-signing consent. Removing that reservation or assigning those tags is a
protocol-review event.

Hosts built against the earlier experimental schema contract used tags 5, 6,
and 7. Protobuf treats those fields as unknown on RC21, so the transaction falls
back to the ordinary unverified review without a transport error. This is safe
but operationally silent. Vault, SDK, and python-keepkey release notes must
therefore state that reusable schemas require the RC21 protocol contract; old
hardware-test fixtures are not compatible evidence.

## Dependency provenance

The selected RC21 firmware commit must pin and record the exact reachable
commits for both submodules. The reviewed candidate pins:

- device-protocol: `4cc8b717517c79ee3ac436161141dd033db286fd`;
- python-keepkey: `c406a1ba9120da410c356dbff7f4d4bd1e1758fa`.

The PR body, gitlinks, CI checkout, and release provenance must agree before the
tag is created.

## Production-key provisioning runbook

The attestor key derives from the active seed/passphrase session. A hidden
wallet therefore has a different attestor identity from the standard wallet.
This is valid for self-service use, but it is not acceptable ambiguity when
selecting KeepKey's future firmware-pinned production identity.

Provision that identity only as follows:

1. install a signed firmware release that contains the attestor;
2. enter the standard wallet with an empty passphrase;
3. enable `AdvancedMode` and read the compressed attestor public key;
4. record the complete public key, its SHA-256 fingerprint, device identity,
firmware version/hash, and the signed release provenance;
5. clear the session, reconnect to the standard wallet with an empty
passphrase, and derive the key again; and
6. accept the candidate only if both complete public keys match byte-for-byte.

Do not provision while a hidden wallet is active. Do not promote a key from
RC21 into firmware merely because it was derived successfully; promotion is a
separate reviewed release decision.

## Merge and tag gate

- Both firmware variants build and satisfy the SRAM reserve.
- Full and Bitcoin-only unit suites pass.
- Python integration passes with inherited `PYTHONPATH` preserved.
- Protocol generation succeeds from the exact pinned device-protocol commit.
- Maximum-boundary OLED captures satisfy the human-attestation gate above.
- The PR body names the exact gitlinks in the selected candidate.
- No RC21 artifact contains a pinned production ClearSign public key or a
persistent runtime signer.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions docs/security/evidence/7.15.0-rc21-clearsign-attestor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# RC21 ClearSign Attestor OLED Evidence

These are pixel captures from the firmware emulator's real 256x64 framebuffer,
read through `DebugLinkGetState`; they are not UI mockups. The boundary schema
contains a 44-character base58 program ID, an 8-byte discriminator, every
supported argument type, 20-character program and instruction names,
16-character argument labels, and a 16-character account label.

The sequence proves that each security-relevant declaration is independently
reviewable before the device returns an attestation signature:

1. `01-schema-identity.png` — full 20-character program and instruction names;
2. `02-program-id-44chars.png` — complete program ID across two rows;
3. `03-discriminator-8bytes.png` — complete 16-hex-character discriminator;
4. `04-arg-u64-le-16char-label.png` — `u64 LE` and its full label;
5. `05-arg-u8-16char-label.png` — `u8` and its full label;
6. `06-arg-public-key-16char-label.png` — `public key` and its full label;
7. `07-arg-bytes32-hex-16char-label.png` — `bytes32 hex` and its full label;
8. `08-account-16char-label.png` — account index and its full label.

`manifest.json` records the exact firmware and device-protocol commits and all
boundary inputs. Reproduce the capture against an isolated emulator with:

```sh
python3 scripts/emulator/capture-clearsign-attestor.py \
--main 127.0.0.1:12044 \
--debug 127.0.0.1:12045 \
--output docs/security/evidence/7.15.0-rc21-clearsign-attestor
```

The capture tool wipes and initializes the addressed emulator. Never point it
at hardware or an emulator instance containing state that must be preserved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"account": {
"index": 7,
"label": "VaultAccount1234",
"label_characters": 16
},
"arguments": [
{
"label": "Amount1234567890",
"label_characters": 16,
"type": "u64 LE"
},
{
"label": "Flag123456789012",
"label_characters": 16,
"type": "u8"
},
{
"label": "RecipientPubKey1",
"label_characters": 16,
"type": "public key"
},
{
"label": "OrderHash1234567",
"label_characters": 16,
"type": "bytes32 hex"
}
],
"attestation_public_key_bytes": 33,
"attestation_signature_bytes": 64,
"device_protocol_commit": "4cc8b717517c79ee3ac436161141dd033db286fd",
"discriminator_bytes": 8,
"discriminator_hex": "0d9e0ddf5fd51c06",
"firmware_commit": "c49bd9d04f981da103b8b3161dbb5b042367f1ce",
"instruction_name": "Review All Types 123",
"instruction_name_characters": 20,
"program_id": "99vQwtBwYtrqqD9YSXbdum3KBdxPAVxYTaQ3cfnJSrN2",
"program_id_characters": 44,
"program_name": "Boundary Program 123",
"program_name_characters": 20,
"screens": [
"01-schema-identity.png",
"02-program-id-44chars.png",
"03-discriminator-8bytes.png",
"04-arg-u64-le-16char-label.png",
"05-arg-u8-16char-label.png",
"06-arg-public-key-16char-label.png",
"07-arg-bytes32-hex-16char-label.png",
"08-account-16char-label.png"
]
}
4 changes: 4 additions & 0 deletions include/keepkey/firmware/fsm.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ void fsm_msgEthereum712TypesValues(Ethereum712TypesValues* msg);
void fsm_msgEthereumTxMetadata(const EthereumTxMetadata* msg);
void fsm_msgLoadClearsignSigner(const LoadClearsignSigner* msg);

void fsm_msgClearsignAttestorGetPublicKey(
const ClearsignAttestorGetPublicKey* msg);
void fsm_msgClearsignAttestorSign(const ClearsignAttestorSign* msg);

void fsm_msgNanoGetAddress(NanoGetAddress* msg);
void fsm_msgNanoSignTx(NanoSignTx* msg);

Expand Down
48 changes: 39 additions & 9 deletions lib/board/signatures.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
#include "trezor/crypto/sha2.h"
#include "trezor/crypto/ecdsa.h"
#include "trezor/crypto/secp256k1.h"
#include "trezor/crypto/memzero.h"
#include "keepkey/board/memory.h"
#include "keepkey/board/memcmp_s.h"
#include "keepkey/board/signatures.h"
#include "keepkey/board/pubkeys.h"

Expand Down Expand Up @@ -68,23 +70,51 @@ int signatures_ok(void) {
return KEY_EXPIRED;
} /* Expired signing key */

/* F3 hardening: double-compute SHA-256, compare in constant time */
uint8_t firmware_fingerprint2[32];
sha256_Raw((uint8_t*)FLASH_APP_START, codelen, firmware_fingerprint);
asm volatile("" ::: "memory");
sha256_Raw((uint8_t*)FLASH_APP_START, codelen, firmware_fingerprint2);

if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex1 - 1],
(uint8_t*)FLASH_META_SIG1,
firmware_fingerprint) != 0) { /* Failure */
if (memcmp_s(firmware_fingerprint, firmware_fingerprint2, 32) != 0) {
memzero(firmware_fingerprint, sizeof(firmware_fingerprint));
memzero(firmware_fingerprint2, sizeof(firmware_fingerprint2));
return SIG_FAIL;
}
memzero(firmware_fingerprint2, sizeof(firmware_fingerprint2));

if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex2 - 1],
(uint8_t*)FLASH_META_SIG2,
firmware_fingerprint) != 0) { /* Failure */
/* F3 hardening: infective aggregation — accumulate all three ECDSA
* results instead of early-returning on each. Forces attacker to
* corrupt all three verify calls, not just skip one branch. */
volatile int verify_acc = 0;
volatile int verify_sentinel = 0;

verify_acc |=
ecdsa_verify_digest(&secp256k1, pubkey[sigindex1 - 1],
(uint8_t*)FLASH_META_SIG1, firmware_fingerprint);
verify_sentinel++;
asm volatile("" ::: "memory");

verify_acc |=
ecdsa_verify_digest(&secp256k1, pubkey[sigindex2 - 1],
(uint8_t*)FLASH_META_SIG2, firmware_fingerprint);
verify_sentinel++;
asm volatile("" ::: "memory");

verify_acc |=
ecdsa_verify_digest(&secp256k1, pubkey[sigindex3 - 1],
(uint8_t*)FLASH_META_SIG3, firmware_fingerprint);
verify_sentinel++;
asm volatile("" ::: "memory");

memzero(firmware_fingerprint, sizeof(firmware_fingerprint));

/* All three verifies must have executed and all must have passed */
if (verify_sentinel != 3) {
return SIG_FAIL;
}

if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex3 - 1],
(uint8_t*)FLASH_META_SIG3,
firmware_fingerprint) != 0) { /* Failure */
if (verify_acc != 0) {
return SIG_FAIL;
}

Expand Down
2 changes: 2 additions & 0 deletions lib/firmware/fsm.c
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ void fsm_msgClearSession(ClearSession* msg) {
#include "fsm_msg_tron.h"
#include "fsm_msg_ton.h"
#include "fsm_msg_solana.h"
/* After fsm_msg_solana.h: reuses its base58 helper and the KKSOLSC1 parser. */
#include "fsm_msg_clearsign_attestor.h"
#if ZCASH_PRIVACY
#include "fsm_msg_zcash.h"
#else
Expand Down
Loading
Loading