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"
]
}
14 changes: 10 additions & 4 deletions include/keepkey/firmware/eip712.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
Parser wants to see C strings, not javascript strings:
requires all complete json message strings to be enclosed by braces,
i.e., { ... } Cannot have entire json string quoted, i.e., "{ ... }" will not
work. Remove all quote escape chars, e.g., {"types": not {\"types\": int
values must be hex. Negative sign indicates negative value, e.g., -5, -8a67
Note: Do not prefix ints or uints with 0x
work. Remove all quote escape chars, e.g., {"types": not {\"types\":
Integer values must use canonical base-10 digits. Negative values use a
leading minus sign. Do not prefix ints or uints with 0x.
All hex and byte strings must be big-endian
Byte strings and address should be prefixed by 0x
*/
Expand Down Expand Up @@ -95,10 +95,16 @@ typedef enum { DOMAIN = 1, MESSAGE } dm;
#define JSON_TYPE_T_NOVAL 31
#define ADDR_STRING_NULL 32
#define JSON_TYPE_WNOVAL 33
#define USER_CANCELLED 34

#define LAST_ERROR JSON_TYPE_WNOVAL
#define LAST_ERROR USER_CANCELLED

int encode(const json_t* jsonTypes, const json_t* jsonVals, const char* typeS,
uint8_t* hashRet);

/* Exposed for strict-value regression tests. */
int encAddress(const char* string, uint8_t* encoded);
int encodeBytes(const char* string, uint8_t* encoded);
int encodeBytesN(const char* typeT, const char* string, uint8_t* encoded);

#endif
3 changes: 3 additions & 0 deletions include/keepkey/firmware/ethereum.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ typedef struct _CoinType CoinType;
void ethereum_signing_init(EthereumSignTx* msg, const HDNode* node,
bool needs_confirm);
void ethereum_signing_abort(void);
bool ethereum_signing_isInProgress(void);
void ethereum_signing_txack(EthereumTxAck* tx);
void format_ethereum_address(const uint8_t* to, char* destination_str,
uint32_t destination_str_len);
Expand Down Expand Up @@ -70,6 +71,8 @@ void bn_from_bytes(const uint8_t* value, size_t value_len, bignum256* val);
void ethereum_typed_hash_sign(const EthereumSignTypedHash* msg,
const HDNode* node,
EthereumTypedDataSignature* resp);
bool ethereum_typed_hash_policy_allows(bool advanced_mode);
bool ethereum_eip712_is_domain_primary_type(const char* primary_type);
bool ethereum_path_check(uint32_t address_n_count, const uint32_t* address_n,
bool pubkey_export, uint64_t chain);
void e712_types_values(Ethereum712TypesValues* msg,
Expand Down
28 changes: 27 additions & 1 deletion include/keepkey/firmware/ethereum_contracts/thortx.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,31 @@
"\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee" \
"\xee\xee"

#define THOR_ROUTER "42a5ed456650a09dc10ebc6361a7480fdd61f27b"
/* THORChain ETH router (mainnet), current v4.1.1.
* NOTE: THORChain migrates this router periodically (v1 42a5ed.. -> v3
* 3624525.. -> v4 d37bbe..). A hardcoded pin must be updated on each migration;
* the durable path is the signed-metadata clear-sign protocol (host-signed,
* key-pinned) which needs no firmware update per router change. */
#define THOR_ROUTER "d37bbe5744d730a1d98d8dc97c42f0ca46ad7146"

/* THORChain deploys its Router at a DIFFERENT address on every EVM chain, so
* the pin must be chain-scoped (see thor_router_for_chain): a deposit on any
* chain but mainnet can never match THOR_ROUTER and would fall to the
* blind-sign gate. Avalanche C-Chain router, verified live against THORChain
* /inbound_addresses via a Pioneer quote (2026-07). Lowercase, no 0x, to match
* thor_format_to_addr's output. Same migration caveat as THOR_ROUTER.
* ponytail: BSC (chainId 56) and Base (8453) routers also exist on-chain but
* are omitted until verified against a live node — the shipped Pioneer catalog
* lists STALE addresses (its AVAX entry 8f66c4ae.. is already wrong vs the live
* 00dc6100..), and Pioneer currently routes BSC/Base swaps via Relay, not a
* THORChain deposit, so no such tx reaches the device today. Add each here once
* verified live. */
#define THOR_ROUTER_AVAX "00dc6100103bc402d490aee3f9a5560cbd91f1d4"

/* Maya Protocol ETH router v4 (mainnet), verified on Etherscan
* (0xe3985e6b61b814f7cdb188766562ba71b446b46d). The prior pin
* d89dce57.. has never held contract code on mainnet. */
#define MAYA_ROUTER "e3985e6b61b814f7cdb188766562ba71b446b46d"

/* deposit(address,address,uint256,string) — legacy selector */
#define THOR_SELECTOR_DEPOSIT "\x1f\xec\xe7\xb4"
Expand All @@ -43,6 +67,8 @@ typedef struct _EthereumSignTx EthereumSignTx;
bool thor_has_deposit_selector(const EthereumSignTx* msg);
bool thor_is_expiry_variant(const EthereumSignTx* msg);
bool thor_isThorchainTx(const EthereumSignTx* msg);
bool thor_isMayachainTx(const EthereumSignTx* msg);
bool thor_confirmThorTx(uint32_t data_total, const EthereumSignTx* msg);
bool thor_confirmMayaTx(uint32_t data_total, const EthereumSignTx* msg);

#endif
3 changes: 3 additions & 0 deletions include/keepkey/firmware/ethereum_contracts/zxliquidtx.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>

#define UNISWAP_ROUTER_ADDRESS \
"\x7a\x25\x0d\x56\x30\xB4\xcF\x53\x97\x39\xdF\x2C\x5d\xAc\xb4\xc6\x59\xF2" \
Expand All @@ -31,5 +32,7 @@ typedef struct _EthereumSignTx EthereumSignTx;

bool zx_isZxLiquidTx(const EthereumSignTx* msg);
bool zx_confirmZxLiquidTx(uint32_t data_total, const EthereumSignTx* msg);
bool zx_formatZxLiquidityPrimaryAmount(const EthereumSignTx* msg, char* out,
size_t out_len);

#endif
10 changes: 7 additions & 3 deletions include/keepkey/firmware/ethereum_tokens.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
#include <stdbool.h>
#include <stdint.h>

#if BITCOIN_ONLY
#define TOKENS_COUNT 0 // no ERC-20 tokens in the bitcoin-only image
#else
enum {
#define X(CHAIN_ID, CONTRACT_ADDR, TICKER, DECIMALS) \
CONCAT(TokenIndex, __COUNTER__),
Expand All @@ -35,11 +38,12 @@ enum {
};

#define TOKENS_COUNT ((int)TokenIndexLast - (int)TokenIndexFirst)
#endif

typedef struct _TokenType {
const char* const address;
const char* const ticker;
uint8_t chain_id;
uint32_t chain_id;
uint8_t decimals;
} TokenType;

Expand All @@ -51,7 +55,7 @@ extern const TokenType* UnknownToken;

const TokenType* tokenIter(int32_t* ctr);

const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address);
const TokenType* tokenByChainAddress(uint32_t chain_id, const uint8_t* address);

/// Tokens don't have unique tickers, so this might not return the one you're
/// looking for :/
Expand All @@ -64,7 +68,7 @@ const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address);
/// \param[out] token The found token, assuming it was uniquely determinable.
/// \returns true iff the token can be uniquely found in the list of known
/// tokens.
bool tokenByTicker(uint8_t chain_id, const char* ticker,
bool tokenByTicker(uint32_t chain_id, const char* ticker,
const TokenType** token);

void coinFromToken(CoinType* coin, const TokenType* token);
Expand Down
6 changes: 6 additions & 0 deletions include/keepkey/firmware/fsm.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ void fsm_msgEthereumSignMessage(EthereumSignMessage* msg);
void fsm_msgEthereumVerifyMessage(const EthereumVerifyMessage* msg);
void fsm_msgEthereumSignTypedHash(const EthereumSignTypedHash* msg);
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
Loading
Loading