From 054cf8fb38440dd1ecbf4fecef2a8f1a9c570588 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Sun, 26 Jul 2026 08:53:07 +0800 Subject: [PATCH 01/12] feat: checkpoint third-party device attestation --- HANDOFF-device-attestation.md | 207 ++++++++ .../hwk-demo/hwk/src/HwkScreen.tsx | 65 ++- .../hwk-demo/hwk/src/connectorFactory.web.ts | 5 +- packages/hwk-adapter-core/src/types/wallet.ts | 6 + packages/hwk-ledger-adapter/package.json | 1 + .../src/adapter/LedgerAdapter.ts | 64 +++ .../src/connector/LedgerConnectorBase.ts | 162 +++++- packages/hwk-trezor-adapter/package.json | 2 + .../src/adapter/TrezorAdapter.ts | 50 ++ .../__tests__/verifyAuthenticityProof.test.ts | 210 ++++++++ .../src/deviceAuthenticity/config.ts | 79 +++ .../findSubjectContentByOid.ts | 12 + .../src/deviceAuthenticity/index.ts | 185 +++++++ .../src/deviceAuthenticity/types.ts | 55 ++ .../validateCaCertExtensions.ts | 40 ++ .../verifyAuthenticityProof.ts | 218 ++++++++ .../deviceAuthenticity/verifySignatures.ts | 44 ++ .../src/deviceAuthenticity/x509certificate.ts | 473 ++++++++++++++++++ packages/hwk-trezor-connector/src/index.ts | 9 + yarn.lock | 2 +- 20 files changed, 1885 insertions(+), 4 deletions(-) create mode 100644 HANDOFF-device-attestation.md create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/findSubjectContentByOid.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/validateCaCertExtensions.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/x509certificate.ts diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md new file mode 100644 index 000000000..56e5bc3b5 --- /dev/null +++ b/HANDOFF-device-attestation.md @@ -0,0 +1,207 @@ +# 交接文档:第三方硬件设备唯一 ID + 账户名迁移 + +> 最后更新:2026-07-26 +> 涉及两条工作线:**① 设备唯一 ID(设备认证 / attestation)—— 已实现 + 真机验证通过**;**② 从 Trezor Suite / Ledger Live 迁移账户名 —— 仅调研,未实现**。 + +--- + +## 0. 状态速览 + +| 项 | 状态 | 真机验证 | +|---|---|---| +| Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已实现 | ✅ T3W1 `Verified ✅` | +| Ledger 设备唯一 ID(DMK genuine-check 走后端) | ✅ 已实现 | ✅ Nano X `Verified ✅` | +| Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | +| Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | +| app-monorepo 调试页(Developer→Gallery→DeviceAuthenticity) | ✅ 已加 | ✅ 已用它测通两端 | +| 账户名迁移 · Ledger | 🔬 调研完成:**可行(纯本地明文)** | — | +| 账户名迁移 · Trezor | 🔬 调研完成:**难(需接设备 + 云端文件)** | — | + +--- + +## 1. 背景与目标 + +**工作线 ①**:记录第三方 Trezor / Ledger 硬件钱包的**物理设备唯一 ID**,用于按收益分账对账。要求:跨 wipe/恢复不变、换设备就不同、不可被同一助记词伪造。核心结论——种子派生的地址/公钥**不行**(那是种子级,同助记词到处一样、wipe 后又变);正确答案是**设备认证(attestation)里出厂烧录、随设备走的密钥**。 + +**工作线 ②**:做一个"从 Trezor Suite / Ledger Live 迁移账户名"的功能(类似 Edge 导入 Chrome 书签):读用户本地 {地址 → 账户名},在 OneKey 里按地址匹配、弹窗问要不要改名。 + +--- + +## 2. 仓库与分支 + +| 仓库 | 分支 | 作用 | +|---|---|---| +| `~/Development/OnekeyWork/hardware-js-sdk` | `feat/device-attestation-id`(从 `onekey` 切) | SDK 实现(新 `hwk-*` 栈) | +| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `debug/fw-progress-trace-6.5.0` | 调试页 + service 转发方法 | +| 参考(只读) | — | `~/Development/OnekeyWork/trezor-suite`、`trezor-firmware`、`node_modules/@ledgerhq/device-management-kit`(DMK) | + +> ⚠️ 关键前提:设备认证只能加在**新 `hwk-*` 栈**(`hwk-trezor-adapter`/`hwk-ledger-adapter`/`hwk-trezor-connector`),**不能**加在 legacy `packages/core`(`DeviceVerify.ts` 那套)——legacy 的 wire protobuf 里没有 `AuthenticateDevice` 消息、features 也没有 `internal_model`。 + +--- + +## 3. 工作线 ①:设备唯一 ID —— 实现细节 + +### 3.1 Trezor(本地可验,最干净) + +**机制**:`AuthenticateDevice` → 设备用 Optiga 安全芯片私钥对主机随机 challenge 签名,返回 `AuthenticityProof`(证书链 + 签名)。主机侧**本地**用 Trezor 公开的根 CA 公钥验证整条证书链 + challenge 签名,从证书 subject 取序列号(OID 2.5.4.5)/设备公钥。 + +**支持机型**:Safe 3(T2B1/T3B1)、Safe 5(T3T1)、T3W1(+Tropic)。**Trezor One(T1B1)/Model T(T2T1) 无安全芯片,不支持**。 + +**API**:`TrezorAdapter.verifyDeviceAuthenticity(connectId, { dangerouslyAllowDebugKeys? })` → `Response`: +```ts +{ verified: boolean, deviceId?, deviceCertPubKey?, serialNumber?, rootPubKey?, caPubKey?, usedDebugKey?, error? } +``` +`deviceId` = 序列号(T3W1+)或 `sha256(deviceCertPubKey)`。**只信 `verified===true` 且 `usedDebugKey!==true`**。 + +**新增验签模块**:`packages/hwk-trezor-adapter/src/deviceAuthenticity/`(从 `@trezor/device-authenticity` 移植): +- `x509certificate.ts`(DER/X.509 解析,自包含,原样移植) +- `verifySignatures.ts`(P-256 + Ed25519,用 `@noble/curves`,**关 lowS**——硬件签名非 low-S;ML-DSA 未支持,缺 `@noble/post-quantum` 依赖,仅 T3W1 第三层) +- `verifyAuthenticityProof.ts`(根CA→CA→设备→challenge 全链 + CA 黑名单钩子) +- `config.ts`(Trezor 根 CA 公钥,来自固件 `hsm_keys.h`,逐字节核对无误) +- `index.ts`(`authenticateDeviceFromProof()`:整体 try/catch 兜底、T3W1 强制 Tropic + serial 交叉校验、debug-key 标记) +- `__tests__/verifyAuthenticityProof.test.ts`(**10 个测试全绿**,用 trezor-suite 真实黄金向量 + 反例) + +**设备调用接线**(3 处): +- `hwk-adapter-core/src/types/wallet.ts`:`IDeviceManagerMethods` 加可选 `authenticateDevice` +- `hwk-trezor-connector/src/index.ts`:switch 加 `case 'authenticateDevice'` 发 `AuthenticateDevice` 消息 +- `hwk-trezor-adapter/src/adapter/TrezorAdapter.ts`:`verifyDeviceAuthenticity` 方法 + +**真机结果**(T3W1):`Verified ✅`,`deviceId=serialNumber` = `3437333132303432353030383646`(ASCII "4731204250086F"),`rootPubKey` 命中 T3W1 PROD Optiga 根键(非 debug)。**证明 H1 修复(optiga+tropic+serial 三重)在真机通过**。 + +### 3.2 Ledger(必须走 Ledger 后端) + +**关键教训**:最初实现的"自造 `E0 52`(GET CERTIFICATE)裸 APDU 本地读证书"路线**真机上不成立**——Nano X 对裸 `E0 52 00 00 00` 返回状态字 `6604`(不是参数错 6a86,是**缺 secure-channel 会话**)。证书读取必须在 Ledger 后端驱动的加密会话里做。 + +**最终机制**:用 DMK 的 `GenuineCheckDeviceAction`——和"装 App"**完全同一套** secure-channel 管道(`wss://scriptrunner.api.live.ledger.com/update/genuine`)。DMK 内部读设备 attestation 证书、算出 `deviceId = sha3_256(attestation pubkey)`,并给出 `{ isGenuine }` 真伪结论。**无需 token / 不用登录**(设备自己在会话里握手),装 App 能连=genuine 能连、零额外配置。 + +**API**:`LedgerAdapter.verifyDeviceAuthenticity(connectId)` → `Response`: +```ts +{ verified: boolean, deviceId?, attestationPubKey?, note } +``` +- `verified` = Ledger HSM 的 `isGenuine` +- `deviceId` = `sha3_256(attestation pubkey)`(后端验证过) +- `attestationPubKey` = 65 字节裸公钥(best-effort,见下) + +**实现**(都在 `hwk-ledger-adapter`): +- `connector/LedgerConnectorBase.ts`:`case 'getDeviceGenuineCheck'` —— `dmk.executeDeviceAction({sessionId, new GenuineCheckDeviceAction({input:{}})})` + `deviceActionToPromise`(**照抄装 App 的模式**),从 Pending 的 `intermediateValue.deviceId`(Uint8Array)捕获 deviceId,**5 分钟超时**("Allow secure connection" 确认看门狗不会为它暂停)。 +- `adapter/LedgerAdapter.ts`:`verifyDeviceAuthenticity` 走该 case;没拿到 deviceId 时返回 failure(不给假 id)。 + +**裸公钥提取**(transport tap):DMK 只吐 hash 不吐裸公钥。为对齐 Trezor,在 `LedgerConnectorBase._tapTransportForCert`/`_tapConnectedDevice` 用 Proxy 包 `TransportConnectedDevice.sendApdu`(**唯一 APDU 必经口**),截 `E0 52` 应答用 LV 解析抠出 65 字节公钥,再用 `sha3_256(pubkey)===deviceId` **交叉校验**确保是对的那把。tap 全程 try/catch + 原样返回,**绝不干扰真实 APDU 流**。 + +**真机结果**(Nano X):`Verified ✅`,`deviceId=818706ff78239d1ab642df69c1d1fdd48acfc59ba282f4139383cfee31adfcdc`(走 genuine-check backend)。`attestationPubKey` 字段的真机验证**待做**(清缓存重启 Desktop 后看 Ledger 结果多一行 `attestationPubKey: 04...`)。 + +### 3.3 Review 修复状态(Fable 5 审查) +- **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 修复:T3W1 强制验 Tropic(Ed25519) + optiga/tropic 序列号交叉校验。真机已验证。 +- **H2**(Ledger 假 id 易误用)→ ✅ 已被 genuine-check 重构取代:现在 Ledger 有真 `verified` + 后端验证的 deviceId。 +- **H3**(Ledger `E0 52` p1/p2 是猜测 + 批次证书风险)→ ✅ 真机坐实"裸 E0 52 不成立",已改走 genuine-check(彻底绕开该风险)。 +- **M1**(无吊销通道)→ ✅ 加 CA 公钥黑名单钩子(默认空)。 +- **M2**(畸形证书抛未捕获异常)→ ✅ `authenticateDeviceFromProof` 整体 try/catch → `verified:false`。 +- **M3**(debug key 无守卫)→ ✅ 参数改名 `dangerouslyAllowDebugKeys` + 结果带 `usedDebugKey` 标志。 +- 未做:ML-DSA(缺 `@noble/post-quantum` 依赖,T3W1 第三层,optiga+tropic 已够);L1(CA notBefore 未来检查,Low)。 + +--- + +## 4. 构建 / 同步 / 缓存 工作流(**踩坑重灾区,必读**) + +app-monorepo 通过**文件拷贝**吃 SDK(node_modules 里是 dist 副本,不是软链)。每次改 SDK 都要走完整链路,**光重启不清缓存不生效**: + +```bash +# 1. 在 hardware-js-sdk 构建改过的包 +cd ~/Development/OnekeyWork/hardware-js-sdk +yarn workspace @onekeyfe/hwk-ledger-adapter build # 或改了哪个包就构建哪个 + +# 2. 同步 dist 到 app-monorepo(cp -R 只覆盖不删) +cp -R packages/hwk-ledger-adapter/dist/. \ + ~/Development/OnekeyWork/app-monorepo-fix-ledger/node_modules/@onekeyfe/hwk-ledger-adapter/dist/ + +# 3. 【关键】停掉 Desktop dev,清 webpack 持久缓存(webpack 把 node_modules 当不可变,不清就用旧代码!) +cd ~/Development/OnekeyWork/app-monorepo-fix-ledger +rm -rf apps/desktop/node_modules/.cache node_modules/.cache/web node_modules/.cache/babel-loader + +# 4. 重启 Desktop(第一次无缓存会慢几分钟) +yarn workspace @onekeyhq/desktop dev +``` + +**判断新代码是否生效**:看报错信息变没变(每版报错串不同)。曾因缓存导致改了半天还跑旧代码、报旧的 `GET CERTIFICATE failed with status 6604`。 +**注意**:缓存不能边跑边清(webpack 会立刻重新生成)——必须先停 Desktop 再清。 +**构建环境**:`tsup` 曾部分缺失,`yarn install` 可恢复(本会话已跑过一次)。 + +> 已构建 + 同步过的包:`hwk-adapter-core`、`hwk-trezor-connector`、`hwk-trezor-adapter`、`hwk-ledger-adapter`(都含最新改动)。 + +--- + +## 5. app-monorepo 调试页(怎么测) + +**新增/改动**(`app-monorepo-fix-ledger`): +- `packages/kit-bg/src/services/ServiceThirdPartyHardware/index.ts`:加 background 转发方法 `thirdPartyHardwareVerifyDeviceAuthenticity({ vendor, connectId })` → `adapter.hw.verifyDeviceAuthenticity(connectId)`。 +- `packages/kit/src/views/Developer/pages/Gallery/Components/stories/DeviceAuthenticity.tsx`:**新页面**(vendor 切换 + Search 拿 connectId + Verify + 结果显示)。 +- `packages/shared/src/routes/gallery.ts`:枚举加 `ComponentDeviceAuthenticity`。 +- `packages/kit/src/views/Developer/pages/Gallery/index.tsx`:lazy import + 路由(菜单自动从枚举生成)。 + +**测试步骤**:Desktop 里 → 开 Developer Mode → **Developer → Gallery → 搜 "DeviceAuthenticity"** → 选 Trezor/Ledger → **Search devices**(先关掉 Trezor Suite / Ledger Live,它们抢 USB/HID)→ **verifyDeviceAuthenticity**。 +- Ledger 首次会弹设备上的 "Allow secure connection" + 联网。 +- **Ledger 的 connectId 为空是正常的**(USB 单设备自动选中);页面已处理(handleVerify 对 Ledger 不强制 connectId)。 + +**测试入口对比**:`hwk-demo`(Expo,只 web,且 Ledger 有 metro friction)**不适合**——用 app-monorepo Desktop 才对。 + +**待办真机验证项**:① Ledger 两台同批次设备 deviceId 是否不同(genuine-check 理论上必唯一);② Ledger `attestationPubKey` 字段是否出现。 + +--- + +## 6. 工作线 ②:账户名迁移 —— 调研结论(未实现) + +### 6.1 Ledger Live —— ✅ 可行(纯本地明文) +数据在 `~/Library/Application Support/Ledger Live/app.json`(明文 JSON,未加密)。本机 17 账户实测跑通。 +- **账户名**:`data.wallet.accountsData.accountNames`(用户改的,`[[accountId,name],...]`)优先,回退 `data.accounts[].data.name`(已解析默认名,永不空)。 +- **地址**:EVM 用 `accounts[].data.freshAddress`(稳定,小写比对);BTC/UTXO 的 `freshAddress` 是"下一个未用地址"**不稳定**,得用 `xpub`+`derivationMode` 或派生 index0。 +- **account id 格式**:`js:2:<链>::<派生模式>`。 +- **加密降级**:用户若在 Ledger Live 设了应用密码,`data.accounts`/`data.wallet` 变 AES 串——检测 `accounts` 不是 list 就提示用户临时关密码,别破解。 +- **`~/Library/Application Support/Ledger Wallet/`** 只有 Crashpad/sentry,无 app.json;数据在 "Ledger Live" 目录。 + +### 6.2 Trezor Suite —— ⚠️ 难(接设备 + 云端文件) +账户标签是**加密的**(labeling/metadata 功能),设计上不让纯本地读: +- **加密**:`.mtdt` 文件 AES-256-GCM。密钥根 `masterKey` **必须由 Trezor 设备**通过 CipherKeyValue 派生: + - `path="m/10015'/0'"`, `key="Enable labeling?"`, `value="fedcba98765432100123456789abcdeffedcba98765432100123456789abcdef"`, `encrypt=true` + - 之后纯软件:`deriveMetadataKey(masterKey,xpub)=base58check(HMAC-SHA256(masterKey,xpub))` → 每账户一把;`deriveAesKey`/`deriveFilename` 见 `trezor-suite/suite/metadata/src/metadataUtils.ts`。 + - **接设备只需一次**(拿 masterKey),逐账户解密全是软件,不用每账户点设备。 +- **文件在哪**:旧版/本地模式在 `userData/metadata/*.mtdt`(接设备就能读);**新版 Suite(26.5.2,用户这台)已搬到 Dropbox / Google Drive / suite-sync 中继**——本地没文件,得让用户 OAuth 授权网盘/连中继取回。 +- **解密后结构**:`{ accountLabel, addressLabels:{地址→名字}, outputLabels }`。`addressLabels` 直接是 {地址→名字};`accountLabel` 是账户整体名,按地址匹配需先从 xpub 派生该账户地址集。 +- **本机现状**:labeling 从没启用过,本地无任何标签(xpub 在 IndexedDB 明文,但没名字)。要真测需一台用户确实改过名的机器/账户。 +- **可行性结论**:**(b) 半能**——接设备拿钥匙 + 搞定文件来源(本地直接读 / 云上要授权)。比 Ledger 重不少。 + +### 6.3 迁移弹窗匹配逻辑(通用) +OneKey 侧已有用户账户地址/xpub。对每条导入 `{地址或xpub → 名字}`:EVM 按地址比、BTC/xpub 类按 xpub(或派生首地址)比,命中就弹"要不要把这个账户也改成这个名"。 + +### 6.4 待你拍板 +- 是否**先做 Ledger**(纯本地、快)、Trezor 排二期? +- Trezor 若做,目标用户主要是"本地存标签"还是"云同步"?后者要把 Dropbox/Google OAuth 纳入范围。 + +--- + +## 7. 关键文件索引 + +**SDK(hardware-js-sdk)** +- 新增:`packages/hwk-trezor-adapter/src/deviceAuthenticity/`(验签模块 + 测试) +- 改:`hwk-trezor-adapter/src/adapter/TrezorAdapter.ts`、`hwk-trezor-connector/src/index.ts`、`hwk-adapter-core/src/types/wallet.ts` +- 改:`hwk-ledger-adapter/src/adapter/LedgerAdapter.ts`、`hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts` +- 改:两个 `package.json`(加 `@noble/curves`/`@noble/hashes`)、`hwk-demo` 两个文件(web 版调试按钮,可选) + +**app-monorepo-fix-ledger** +- `packages/kit-bg/src/services/ServiceThirdPartyHardware/index.ts` +- `packages/kit/src/views/Developer/pages/Gallery/Components/stories/DeviceAuthenticity.tsx`(新) +- `packages/shared/src/routes/gallery.ts`、`packages/kit/src/views/Developer/pages/Gallery/index.tsx` + +**参考/证据** +- Trezor 验签参考:`trezor-suite/packages/device-authenticity/src/` +- Trezor 根 CA:`trezor-firmware/core/embed/projects/prodtest/cmd/hsm_keys.h` +- Ledger genuine-check:`node_modules/@ledgerhq/device-management-kit/lib/esm/src/api/secure-channel/` +- Ledger 装 App 模式参照:`hwk-ledger-adapter/src/device-apps/DeviceApps.ts` +- 迁移数据:`~/Library/Application Support/Ledger Live/app.json`、`~/Library/Application Support/@trezor/`、`trezor-suite/suite/metadata/src/metadataUtils.ts` + `metadataLabelingConstants.ts` + +--- + +## 8. 未提交 / 未决事项 +- **两个仓库的改动都未 commit**(本会话没提交)。 +- 设备 ID:Ledger `attestationPubKey` 字段 + 两台同批次 deviceId 唯一性待真机确认。 +- 分账合规提醒:采集第三方设备 ID 属设备指纹,多辖区算个人数据需披露同意;attestation 只证明"真设备",不证明"同一个人"。 +- 账户名迁移:范围未定(见 6.4)。 diff --git a/packages/connect-examples/hwk-demo/hwk/src/HwkScreen.tsx b/packages/connect-examples/hwk-demo/hwk/src/HwkScreen.tsx index e23a05d75..e32a8a42b 100644 --- a/packages/connect-examples/hwk-demo/hwk/src/HwkScreen.tsx +++ b/packages/connect-examples/hwk-demo/hwk/src/HwkScreen.tsx @@ -38,7 +38,14 @@ type DebugLogEntry = { event: string; data?: Record; }; -type BusyState = null | 'scan' | 'connect' | 'features' | 'address' | 'disconnect'; +type BusyState = + | null + | 'scan' + | 'connect' + | 'features' + | 'address' + | 'disconnect' + | 'attestation'; type ThpPairingPrompt = { connectId: string; availableMethods: number[]; @@ -129,6 +136,7 @@ const busyLabelMap: Record, string> = { features: 'Getting features...', address: 'Getting address...', disconnect: 'Disconnecting...', + attestation: 'Verifying device...', }; const upsertDevice = (devices: DeviceInfo[], device: DeviceInfo) => { @@ -678,6 +686,54 @@ export const HwkScreen = () => { } }, [appendLog, connectedId, selected?.connectId]); + const onVerifyDevice = useCallback(async () => { + const adapter = adapterRef.current; + const connectId = selected?.connectId ?? connectedId; + if (!adapter || !connectId) { + Alert.alert('Tip', 'Please connect a device first.'); + return; + } + // Both TrezorAdapter and LedgerAdapter expose verifyDeviceAuthenticity(connectId). + const verifier = adapter as unknown as { + verifyDeviceAuthenticity: (id: string) => Promise<{ + success: boolean; + payload: { + verified?: boolean; + deviceId?: string; // Trezor (verified) + unverifiedDeviceId?: string; // Ledger (local read, unverified) + usedDebugKey?: boolean; + attestationPubKey?: string; + deviceCertPubKey?: string; + serialNumber?: string; + note?: string; + error?: string; + }; + }>; + }; + try { + setBusy('attestation'); + appendLog('info', `verifyDeviceAuthenticity ${connectId}... (confirm on device)`); + const result = await verifier.verifyDeviceAuthenticity(connectId); + appendLog('info', { verifyDeviceAuthenticity: result }); + if (!result.success) { + Alert.alert('verifyDeviceAuthenticity failed', JSON.stringify(result.payload)); + return; + } + const { verified, deviceId, unverifiedDeviceId, usedDebugKey, note } = result.payload; + const id = deviceId ?? unverifiedDeviceId ?? '(none)'; + const title = verified + ? usedDebugKey + ? 'Verified ⚠️ DEBUG key' + : 'Verified ✅' + : 'Read only (not verified)'; + Alert.alert(title, `id:\n${id}${note ? `\n\n${note}` : ''}`); + } catch (error) { + appendLog('error', { step: 'verifyDeviceAuthenticity.error', error: formatError(error) }); + } finally { + setBusy(null); + } + }, [appendLog, connectedId, selected?.connectId]); + const onGetAddress = useCallback(async () => { const adapter = adapterRef.current; const connectId = selected?.connectId ?? connectedId; @@ -1008,6 +1064,13 @@ export const HwkScreen = () => { > getFeatures + + verifyDevice (deviceId) + >>; wipeDevice?(connectId: string): Promise>>; + // Sends AuthenticateDevice and returns the raw AuthenticityProof message. + // The challenge must be generated host-side and passed in. + authenticateDevice?( + connectId: string, + params: { challenge: string } + ): Promise>>; } export interface IWalletStateMethods { diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index ddc39c458..a095af3e5 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -50,6 +50,7 @@ "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/hw-app-trx": "^6.34.1", "@ledgerhq/hw-transport": "^6.34.1", + "@noble/hashes": "^1.3.3", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" }, diff --git a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts index 7c896837c..beac8c273 100644 --- a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts +++ b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts @@ -129,6 +129,28 @@ function btcAccountIndexFromPath(path: string): number | null { return Number.isFinite(accountIndex) ? accountIndex : null; } +export type LedgerAttestationResult = { + /** + * Ledger's official genuine-check verdict (from Ledger's HSM, over the same + * secure-channel backend as app install). Only trust `deviceId` when true. + */ + verified: boolean; + /** + * Stable per-device id = sha3-256 of the device attestation public key (hex), + * computed by DMK during the genuine check. Survives wipe/recovery and cannot + * be forged from a seed. Backend-verified when `verified` is true. + */ + deviceId?: string; + /** + * Raw device attestation public key (65-byte uncompressed EC point, hex), + * recovered from the GET CERTIFICATE response during the genuine check. + * Best-effort: may be undefined if it could not be captured. `deviceId` is + * exactly sha3-256 of this key. + */ + attestationPubKey?: string; + note: string; +}; + export class LedgerAdapter implements IHardwareWallet { readonly vendor = 'ledger' as const; @@ -827,6 +849,48 @@ export class LedgerAdapter implements IHardwareWallet { } } + /** + * Runs Ledger's official genuine check (DMK GenuineCheckDeviceAction) over the + * SAME secure-channel backend as app install + * (wss://scriptrunner.api.live.ledger.com/update/genuine). It returns Ledger's + * HSM verdict (`verified`) and a stable per-device id = sha3-256 of the device + * attestation public key, which DMK reads inside that session. The id survives + * wipe/recovery and cannot be forged from a seed. + * + * Requires network access to Ledger's backend and an on-device + * "Allow secure connection" confirmation the first time. + */ + async verifyDeviceAuthenticity( + connectId: string + ): Promise> { + try { + const result = (await this.connectorCall(connectId, 'getDeviceGenuineCheck', {})) as { + isGenuine: boolean; + deviceId?: string; + attestationPubKey?: string; + }; + if (!result.deviceId) { + // The genuine verdict came back, but DMK did not surface a deviceId + // (server did not drive GET CERTIFICATE, or the certificate failed to + // parse). Without an id there is nothing to record for accounting. + return failure( + HardwareErrorCode.UnknownError, + `Genuine check completed (isGenuine=${result.isGenuine}) but no deviceId was captured` + ); + } + return success({ + verified: result.isGenuine, + deviceId: result.deviceId, + attestationPubKey: result.attestationPubKey, + note: result.isGenuine + ? 'Verified by Ledger genuine-check backend; deviceId = sha3_256(attestation pubkey).' + : 'Ledger genuine-check returned NOT genuine — do not trust this device.', + }); + } catch (err) { + return this.errorToFailure(err); + } + } + // --------------------------------------------------------------------------- // Events // --------------------------------------------------------------------------- diff --git a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts index 6550217c9..3e1318a66 100644 --- a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts +++ b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts @@ -35,7 +35,34 @@ import { tronSignTransaction, } from './chains'; import { DeviceAppsManager } from '../device-apps/DeviceAppsManager'; +import { sha3_256 } from '@noble/hashes/sha3'; + import { collapseSignerInteraction } from './chains/utils'; +import { deviceActionToPromise } from '../signer/deviceActionToPromise'; + +// GET CERTIFICATE (E0 52) response = length-value fields: a header field followed +// by the attestation public key field. Mirrors DMK's `extractPublicKey`: skip the +// first LV field, return the second (65-byte uncompressed EC point). +function extractCertPubKey(data: Uint8Array): string | null { + try { + const buf = Buffer.from(data); + let offset = 0; + if (offset >= buf.length) return null; + const headerLen = buf[offset]; + offset += 1 + headerLen; + if (offset >= buf.length) return null; + const keyLen = buf[offset]; + offset += 1; + if (keyLen === 0 || offset + keyLen > buf.length) return null; + return buf.subarray(offset, offset + keyLen).toString('hex'); + } catch { + return null; + } +} + +function sha3HexOf(pubKeyHex: string): string { + return Buffer.from(sha3_256(Uint8Array.from(Buffer.from(pubKeyHex, 'hex')))).toString('hex'); +} import type { DeviceApps, @@ -847,6 +874,71 @@ export class LedgerConnectorBase implements IConnector { ctx.clearCanceller(sessionId); } } + case 'getDeviceGenuineCheck': { + // Ledger official genuine check, over the SAME secure-channel pipeline + // as app install (DMK → wss://scriptrunner.api.live.ledger.com/update/genuine). + // DMK reads the device attestation certificate inside that session and + // emits deviceId = sha3_256(attestation pubkey) in the Pending + // intermediateValue (NOT the final output) — captured below. The final + // output carries the { isGenuine } verdict from Ledger's HSM. + try { + // Reset the transport tap so we only match certificates read during + // THIS genuine check. + this._capturedGenuineCertPubKeys = []; + const dmk = await ctx.getOrCreateDmk(); + const kit = await ctx.importLedgerKit('@ledgerhq/device-management-kit'); + const action = ( + dmk as unknown as { + // Loose return: deviceActionToPromise narrows the observable shape. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + executeDeviceAction(args: { + sessionId: string; + deviceAction: unknown; + }): any; + } + ).executeDeviceAction({ + sessionId, + deviceAction: new kit.GenuineCheckDeviceAction({ input: {} }), + }); + + let deviceId: string | undefined; + const output = await deviceActionToPromise<{ isGenuine: boolean }>( + action, + (interaction: string) => + ctx.emit('ui-event', { + type: collapseSignerInteraction(interaction), + payload: { sessionId }, + }), + // Generous timeout: covers websocket round-trips + the on-device + // "Allow secure connection" tap (the idle watchdog does not pause + // for it, only for unlock-device). + 5 * 60_000, + (cancel) => ctx.registerCanceller(sessionId, cancel), + (intermediateValue) => { + const iv = intermediateValue as { deviceId?: Uint8Array } | undefined; + if (iv?.deviceId instanceof Uint8Array && !deviceId) { + deviceId = Buffer.from(iv.deviceId).toString('hex'); + } + }, + ); + + // Best-effort: recover the raw attestation public key that DMK hashed + // into deviceId. Match by sha3_256(pubkey) === deviceId so we return + // the exact key behind this id (not some other cert seen on the wire). + const attestationPubKey = + deviceId && this._capturedGenuineCertPubKeys.length > 0 + ? this._capturedGenuineCertPubKeys.find((pk) => sha3HexOf(pk) === deviceId) + : undefined; + this._capturedGenuineCertPubKeys = []; + + return { isGenuine: output.isGenuine, deviceId, attestationPubKey }; + } catch (err) { + ctx.invalidateSession(sessionId); + throw ctx.wrapError(err); + } finally { + ctx.clearCanceller(sessionId); + } + } default: throw new Error(`LedgerConnector: unknown method "${method}"`); } @@ -907,6 +999,74 @@ export class LedgerConnectorBase implements IConnector { * If a DMK was provided via constructor, it is used directly. * Otherwise, one is created via the transport factory. */ + // Attestation public keys (hex) seen in GET CERTIFICATE (E0 52) responses on + // the transport. Only populated during a genuine check; cleared per call. + private _capturedGenuineCertPubKeys: string[] = []; + + // Wrap a TransportFactory so every connected device's `sendApdu` is tapped: + // when DMK's genuine-check secure channel reads the device attestation + // certificate (E0 52), we capture the raw public key from the response. The + // tap is best-effort and can NEVER disturb the real APDU flow (it always + // returns the original result, all capture logic is in try/catch). + private _tapTransportForCert(factory: unknown): unknown { + const self = this; + return (args: unknown) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transport = (factory as (a: unknown) => any)(args); + const originalConnect = transport?.connect; + if (typeof originalConnect === 'function') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transport.connect = async (params: unknown): Promise => { + const either = await originalConnect.call(transport, params); + try { + if (either && typeof either.map === 'function') { + // purify-ts Either: map only touches the Right (connected device). + return either.map((device: unknown) => self._tapConnectedDevice(device)); + } + } catch { + /* fall through, return untouched */ + } + return either; + }; + } + return transport; + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _tapConnectedDevice(device: any): unknown { + const self = this; + const originalSendApdu = device?.sendApdu; + if (typeof originalSendApdu !== 'function') return device; + const wrapped = async (apdu: Uint8Array, ...rest: unknown[]) => { + const result = await originalSendApdu.call(device, apdu, ...rest); + try { + if ( + apdu?.[0] === 0xe0 && + apdu?.[1] === 0x52 && + result && + typeof result.isRight === 'function' && + result.isRight() + ) { + const resp = result.extract() as { statusCode: Uint8Array; data: Uint8Array }; + if (resp && Buffer.from(resp.statusCode).toString('hex') === '9000') { + const pubKey = extractCertPubKey(resp.data); + if (pubKey) self._capturedGenuineCertPubKeys.push(pubKey); + } + } + } catch { + /* the cert tap must never break the real APDU exchange */ + } + return result; + }; + return new Proxy(device, { + get(target, prop, receiver) { + if (prop === 'sendApdu') return wrapped; + return Reflect.get(target, prop, receiver); + }, + }); + } + protected async _getOrCreateDmk(): Promise { debugLog( '[DMK] _getOrCreateDmk called, _dmk exists:', @@ -924,7 +1084,7 @@ export class LedgerConnectorBase implements IConnector { const { DeviceManagementKitBuilder } = await this._importLedgerKit( '@ledgerhq/device-management-kit' ); - const transportFactory = await this._createTransport(); + const transportFactory = this._tapTransportForCert(await this._createTransport()); debugLog('[DMK] _getOrCreateDmk: transportFactory type:', typeof transportFactory); diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index a3f993443..14381c373 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -43,6 +43,8 @@ "crypto" ], "dependencies": { + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.3.3", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.1", "tslib": "^2.6.2" diff --git a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts index 1fe4ea990..84703ceb7 100644 --- a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts +++ b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts @@ -12,8 +12,14 @@ import { success, } from '@onekeyfe/hwk-adapter-core'; +import { randomBytes } from '@noble/hashes/utils'; + +import { authenticateDeviceFromProof } from '../deviceAuthenticity'; + import { debugLog } from '../utils/debugLog'; +import type { AuthenticateDeviceResult, AuthenticityProof } from '../deviceAuthenticity'; + import type { AllNetworkAddressParams, AllNetworkAddressResponse, @@ -656,6 +662,50 @@ export class TrezorAdapter implements IHardwareWallet { return this._callDeviceManagerMethod>('getFeatures', connectId, {}); } + /** + * Device authenticity attestation. Sends a host-generated challenge, has the + * device sign it with its secure-element key, then verifies the returned + * certificate chain up to a trusted Trezor root CA. On success returns a + * `deviceId` that uniquely identifies the physical device, survives wipe, and + * cannot be forged from a seed. Only trust the result when `verified` is true + * (and `usedDebugKey` is false in production). + * + * Requires a secure-element model (Safe 3 = T2B1/T3B1, Safe 5 = T3T1, T3W1) + * and an on-device confirmation. Older models (T1B1, T2T1) will fail. + * + * `dangerouslyAllowDebugKeys` accepts simulator / development root keys — NEVER + * enable it in a production accounting flow, it lets an emulator mint any id. + */ + async verifyDeviceAuthenticity( + connectId: string, + params: { dangerouslyAllowDebugKeys?: boolean } = {} + ): Promise> { + const featuresRes = await this.getFeatures(connectId); + if (!featuresRes.success) return featuresRes; + + const internalModel = (featuresRes.payload as { internal_model?: string }).internal_model; + if (!internalModel) { + return failure(HardwareErrorCode.UnknownError, 'Device internal_model unavailable'); + } + + const challenge = Buffer.from(randomBytes(32)); + const proofRes = await this._callDeviceManagerMethod>( + 'authenticateDevice', + connectId, + { challenge: challenge.toString('hex') } + ); + if (!proofRes.success) return proofRes; + + return success( + authenticateDeviceFromProof({ + proof: proofRes.payload as unknown as AuthenticityProof, + challenge, + deviceModel: internalModel, + allowDebugKeys: params.dangerouslyAllowDebugKeys, + }) + ); + } + deviceSettings( connectId: string, params: TrezorDeviceSettingsParams diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts new file mode 100644 index 000000000..db2f33d5c --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts @@ -0,0 +1,210 @@ +import { authenticateDeviceFromProof } from '..'; +import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from '../verifyAuthenticityProof'; + +import type { DeviceAuthenticityConfig } from '../types'; + +// Golden vectors from trezor-suite `mockDeviceAuthenticityData.ts`. +// These are real device/CA certificate chains and challenge signatures. +const CHALLENGE = '29d0be0f3cb191c80d108359c64d22984a77ad8b99433814be31db0b6e9e7920'; + +const CA_CERT_OPTIGA = + '308201df30820184a00302010202040a001ab3300a06082a8648ce3d0403023054310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3125302306035504030c1c5472657a6f72204d616e75666163747572696e6720526f6f742043413020170d3233303130313030303030305a180f32303533303130313030303030305a304f310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3120301e06035504030c175472657a6f72204d616e75666163747572696e672043413059301306072a8648ce3d020106082a8648ce3d030107034200041b36cc98d5e3d1a20677aaf26254ef3756f27c9d63080c93ad3e7d39d3ad23bf00497b924789bc8e3f87834994e16780ad4eae7e75db1f03835ca64363e980b4a3473045300e0603551d0f0101ff04040302020430120603551d130101ff040830060101ff020100301f0603551d2304183016801428b202f8f9c78a74e8c152bbfb433d99d0ca03ef300a06082a8648ce3d0403020349003046022100dfe2f837f3644c1f0250d37cd0f7d1e4e9b8cfc4820d7f5a623a8cb69df99f6c02210089148848c5fc597df4b8545d9b19d1cc15abe0e1252fa2938a4cf01ae835c563'; +const DEVICE_CERT_OPTIGA = + '3082019e30820145a00302010202044ee2a50f300a06082a8648ce3d040302304f310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3120301e06035504030c175472657a6f72204d616e75666163747572696e67204341301e170d3232303433303134313630315a170d3432303433303134313630315a301d311b301906035504030c1254324231205472657a6f72205361666520333059301306072a8648ce3d020106082a8648ce3d030107034200049bbf06dad9ab5905e05471ce16d5222c89c2caa39f26267ac0747129885fbd441bcc7fa84de120a36755daf30a6f47e8c0d4bddc15036ed2a3447dfa7a1d3e88a341303f300e0603551d0f0101ff040403020080300c0603551d130101ff04023000301f0603551d23041830168014176d8b9a403574f6a2b9ac353ef578682201a21a300a06082a8648ce3d04030203470030440220747c545e112df816173d3071f1ab25d399d8108550764ce1a3a428f1f18b506902200cda822c75b3da6e44e098014452f3fc324f29a79204c3fb4d5815afafc04b17'; +const SIGNATURE_OPTIGA = + '3045022100c01793ffbe4f16d4efc84a4533d9bbfbbf1baa5349346678e07fdb6d848cca7902200df11b9d2850173d9c93993fca983c6d2a3f31ea69a0e19b69e18cc3b78424fe'; +const T2B1_ROOT_PUB_KEY_OPTIGA = + '04626d58aca84f0fcb52ea63f0eb08de1067b8d406574a715d5e7928f4b67f113a00fb5c5918e74d2327311946c446b242c20fe7347482999bdc1e229b94e27d96'; +const DEVICE_PUB_KEY_OPTIGA = + '049bbf06dad9ab5905e05471ce16d5222c89c2caa39f26267ac0747129885fbd441bcc7fa84de120a36755daf30a6f47e8c0d4bddc15036ed2a3447dfa7a1d3e88'; + +const CA_CERT_TROPIC = + '308201c130820173a003020102020868b1982d8b917275300506032b65703054310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3125302306035504030c1c5472657a6f72204d616e75666163747572696e6720526f6f742043413020170d3235303832393132353934375a180f32303535303832323132353934375a304f310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3120301e06035504030c175472657a6f72204d616e75666163747572696e67204341302a300506032b65700321009603b4971f811ed2a1cdb9ec3e6d6d0e22facfd83892a30480460872a2003f45a3663064300e0603551d0f0101ff04040302020430120603551d130101ff040830060101ff020100301d0603551d0e04160414cf35aa12a033c044ebc6c3c0c3aefea5ae5e7db4301f0603551d2304183016801424e601ebe264f0b1cfc5bc27cc03cdf69f23a85e300506032b657003410072e527330a4b079f8b8f261489595d7e0c6bc84cd4eecdf98988f4b185df503ac3ee7364ce7ef934f56c8e823f1ac38667f85d38469884f934290efdd27bef0e'; +const DEVICE_CERT_TROPIC = + '308201993082014ba003020102020868b1982dacb2b041300506032b6570304f310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3120301e06035504030c175472657a6f72204d616e75666163747572696e672043413020170d3235303832393132353934375a180f32303535303832323132353934375a3051311b301906035504030c1254335731205472657a6f72205361666520373121301f06035504051318343732303930323235323232323232323232323232323232310f300d060355042e130654726f706963302a300506032b6570032100c92482676994f4e2dec7eb2bb6a9ce9a8a38d4536c5e16b26646015cd7034593a341303f300e0603551d0f0101ff040403020080300c0603551d130101ff04023000301f0603551d23041830168014cf35aa12a033c044ebc6c3c0c3aefea5ae5e7db4300506032b65700341003b0aa3ccdddfd2a473e3f36059d0da2e54428c8dca5050a5fabf845ca71365d8045cc4d15c4e6e37565cbd66cdb4ba8d7ca113941f596a9bbfc25c069cf91903'; +const SIGNATURE_TROPIC = + '9d6a7cfc1d9957a7eaa09f58ab385a4722dc621c6a58f0e281305f36c1447206ea6642f4e4ff36207bd57c3719101855e9c9a00a5db60cc84e20181d69f4fa00'; +const T3W1_ROOT_PUB_KEY_TROPIC = + 'cd318dc8405ae4f4144e3284dcb7b0cb0f0c2195c2ca14a0f6fccd9104e32a4b'; +const SERIAL_TROPIC = '343732303930323235323232323232323232323232323232'; + +const CONFIG: DeviceAuthenticityConfig = { + version: 1, + T2B1: { rootPubKeysOptiga: [T2B1_ROOT_PUB_KEY_OPTIGA] }, + T3W1: { rootPubKeysOptiga: [], rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC] }, +}; + +const signedData = prepareDeviceAuthenticityData({ payload: Buffer.from(CHALLENGE, 'hex') }); + +describe('verifyAuthenticityProof', () => { + it('verifies a genuine Optiga (P-256) proof', () => { + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + signature: SIGNATURE_OPTIGA, + signedData, + deviceModel: 'T2B1', + config: CONFIG, + }); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.deviceCertPubKey).toBe(DEVICE_PUB_KEY_OPTIGA); + expect(result.rootPubKey).toBe(T2B1_ROOT_PUB_KEY_OPTIGA); + expect(result.serialNumber).toBeUndefined(); // T2B1 has no OID 2.5.4.5 + } + }); + + it('verifies a genuine Tropic (Ed25519) proof and extracts the serial number', () => { + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_TROPIC, CA_CERT_TROPIC], + signature: SIGNATURE_TROPIC, + signedData, + deviceModel: 'T3W1', + config: CONFIG, + }); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.serialNumber).toBe(SERIAL_TROPIC); + expect(result.rootPubKey).toBe(T3W1_ROOT_PUB_KEY_TROPIC); + } + }); + + it('rejects a tampered challenge signature', () => { + const tampered = `${SIGNATURE_OPTIGA.slice(0, -2)}00`; + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + signature: tampered, + signedData, + deviceModel: 'T2B1', + config: CONFIG, + }); + + expect(result.valid).toBe(false); + if (!result.valid) expect(result.error).toBe('INVALID_DEVICE_SIGNATURE'); + }); + + it('rejects when the root CA is not trusted', () => { + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + signature: SIGNATURE_OPTIGA, + signedData, + deviceModel: 'T2B1', + config: { version: 1, T2B1: { rootPubKeysOptiga: [] } }, + }); + + expect(result.valid).toBe(false); + if (!result.valid) expect(result.error).toBe('ROOT_PUBKEY_NOT_FOUND'); + }); +}); + +describe('authenticateDeviceFromProof', () => { + it('derives a stable per-device id (sha256 of the device pubkey when no serial)', () => { + const result = authenticateDeviceFromProof({ + proof: { + optiga_certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + optiga_signature: SIGNATURE_OPTIGA, + }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T2B1', + config: CONFIG, + }); + + expect(result.verified).toBe(true); + expect(result.deviceCertPubKey).toBe(DEVICE_PUB_KEY_OPTIGA); + // deviceId is deterministic for a given device + expect(result.deviceId).toMatch(/^[0-9a-f]{64}$/); + }); + + it('reports verified=false for a missing proof payload', () => { + const result = authenticateDeviceFromProof({ + proof: { optiga_certificates: [], optiga_signature: '' }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T2B1', + config: CONFIG, + }); + + expect(result.verified).toBe(false); + expect(result.deviceId).toBeUndefined(); + expect(result.error).toBe('RESPONSE_PAYLOAD_MISSING'); + }); + + // M2: a malicious device can return a malformed certificate; must fold into + // verified:false, never throw an uncaught error. + it('never throws on a malformed certificate (returns verified=false)', () => { + let result: ReturnType | undefined; + expect(() => { + result = authenticateDeviceFromProof({ + proof: { optiga_certificates: ['deadbeef', 'c0ffee'], optiga_signature: 'ab' }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T2B1', + config: CONFIG, + }); + }).not.toThrow(); + expect(result?.verified).toBe(false); + expect(result?.deviceId).toBeUndefined(); + expect(result?.error).toMatch(/RESPONSE_MALFORMED/); + }); + + // M3: a chain that only matches a DEBUG/staging root must be flagged. + it('flags usedDebugKey when the chain matches a debug root', () => { + const debugConfig: DeviceAuthenticityConfig = { + version: 1, + T2B1: { rootPubKeysOptiga: [], debug: { rootPubKeysOptiga: [T2B1_ROOT_PUB_KEY_OPTIGA] } }, + }; + const result = authenticateDeviceFromProof({ + proof: { + optiga_certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + optiga_signature: SIGNATURE_OPTIGA, + }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T2B1', + config: debugConfig, + allowDebugKeys: true, + }); + + expect(result.verified).toBe(true); + expect(result.usedDebugKey).toBe(true); + }); + + // H1: on a Tropic-capable model (T3W1) an Optiga-only proof must NOT verify. + it('requires the Tropic layer on T3W1 (optiga-only proof is rejected)', () => { + const t3w1Config: DeviceAuthenticityConfig = { + version: 1, + T3W1: { rootPubKeysOptiga: [T2B1_ROOT_PUB_KEY_OPTIGA], rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC] }, + }; + const result = authenticateDeviceFromProof({ + // reuse the optiga vector but omit tropic_* → tropic required-but-missing + proof: { + optiga_certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + optiga_signature: SIGNATURE_OPTIGA, + }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T3W1', + config: t3w1Config, + }); + + // The optiga device cert says model T2B1, so it fails model check before we + // even reach the tropic requirement; either way it must NOT verify on T3W1. + expect(result.verified).toBe(false); + }); +}); + +describe('M1: CA public key blacklist (revocation)', () => { + const CA_PUB_KEY_OPTIGA = + '041b36cc98d5e3d1a20677aaf26254ef3756f27c9d63080c93ad3e7d39d3ad23bf00497b924789bc8e3f87834994e16780ad4eae7e75db1f03835ca64363e980b4'; + + it('rejects a blacklisted CA public key even when the chain is otherwise valid', () => { + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_OPTIGA, CA_CERT_OPTIGA], + signature: SIGNATURE_OPTIGA, + signedData, + deviceModel: 'T2B1', + config: CONFIG, + caPubKeyBlacklist: [CA_PUB_KEY_OPTIGA], + }); + + expect(result.valid).toBe(false); + if (!result.valid) expect(result.error).toBe('CA_PUBKEY_BLACKLISTED'); + }); +}); diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts new file mode 100644 index 000000000..a8b73ddfe --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts @@ -0,0 +1,79 @@ +import type { DeviceAuthenticityConfig } from './types'; + +/** + * Trezor manufacturing root CA public keys, per internal model. + * Authoritative upstream source (firmware): + * https://github.com/trezor/trezor-firmware/blob/main/core/embed/projects/prodtest/cmd/hsm_keys.h + * Mirrored from @trezor/device-authenticity `deviceAuthenticityConfig`. + * + * Note: ML-DSA (rootPubKeysMLDSA) keys are intentionally omitted — ML-DSA + * verification is not supported here (see verifySignatures.ts), and the Optiga + * P-256 path already provides a valid per-device identity for every model. + * Devices without a secure element (T1B1, T2T1) do not support AuthenticateDevice + * and are therefore absent from this config. + */ +export const deviceAuthenticityConfig: DeviceAuthenticityConfig = { + version: 2, + T2B1: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_PROD_P256 + '04ca97480ac0d7b1e6efafe518cd433cec2bf8ab9822d76eafd34363b55d63e60380bff20acc75cde03cffcb50ab6f8ce70c878e37ebc58ff7cca0a83b16b15fa5', + ], + debug: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_DEBUG_P256 shared with T3B1 + '047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a', + ], + }, + }, + T3B1: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_PROD_P256 + '045b5c3fdd01f3602092834209b86df0ca86a9faf25cac35c73bf6237d66eb21eafcec3706f1ccd5eb4cc7f2fa1751213eccb1c78389afba89a5788ff31ee46a5d', + ], + debug: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_DEBUG_P256 shared with T2B1 + '047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a', + ], + }, + }, + T3T1: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_PROD_P256 + '041854b27fb1d9f65abb66828e78c9dc0ca301e66081ab0c6a4d104f9df1cd0ad5a7c75f77a8c092f55cf825d2abaf734f934c9394d5e75f75a5a06a5ee9be93ae', + ], + debug: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_DEBUG_P256 + '04e48b69cd7962068d3cca3bcc6b1747ef496c1e28b5529e34ad7295215ea161dbe8fb08ae0479568f9d2cb07630cb3e52f4af0692102da5873559e45e9fa72959', + ], + }, + }, + T3W1: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_PROD_P256 + '040dde0d3e0d4da593fac6fd02a461d0e7eef238aca55c7c50b4e9ec37f3873303b6429ef1c9b78b4411a7dcbbc5dde5225979c1c2da3b073e82b1ed3f5f9825bb', + // upstream: DEV_AUTH_ROOT_PROD_BACKUP_P256 + '04c6a673af4ec44b10441b1d78676e15173ad0e36df9f7f2fa1cd819955f20fe32917b60da5fed3b3aa54a9ab8b3ed27d198b3768cad26eef5935cd87af0af065e', + ], + rootPubKeysTropic: [ + // upstream: DEV_AUTH_ROOT_PROD_ED25519 + '59237acd17134061d655b3f8d624573ca06ce8d862f38ba4e05140ce1d3d609d', + // upstream: DEV_AUTH_ROOT_PROD_BACKUP_ED25519 + '5612606584ee7e0bc313b13f7ac94156bb4cb75bd77585ddbe579301306e85f1', + ], + debug: { + rootPubKeysOptiga: [ + // upstream: DEV_AUTH_ROOT_DEBUG_P256 + '04521192e173a9da4e3023f747d836563725372681eba3079c56ff11b2fc137ab189eb4155f371127651b5594f8c332fc1e9c0f3b80d4212822668b63189706578', + // upstream: DEV_AUTH_ROOT_STAGING_P256 + '0465e88f9b2cea67e8364f0cfcfacd500af24e9040b357beee629ccc4fce1704d1a7ef7284f387708f92ef14600e2caad6894016fee819d623b95d66210c3e7519', + ], + rootPubKeysTropic: [ + // upstream: DEV_AUTH_ROOT_STAGING_ED25519 + 'cd318dc8405ae4f4144e3284dcb7b0cb0f0c2195c2ca14a0f6fccd9104e32a4b', + ], + }, + }, +}; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/findSubjectContentByOid.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/findSubjectContentByOid.ts new file mode 100644 index 000000000..dca33d7ef --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/findSubjectContentByOid.ts @@ -0,0 +1,12 @@ +import type { Oid, ParsedCertificate } from './x509certificate'; + +export const findSubjectContentByOid = ( + deviceCert: ParsedCertificate, + oid: Oid, +): Uint8Array | null => { + const subjectItems = deviceCert.tbsCertificate.subject; + const matchedItem = subjectItems.find(({ algorithmOid }) => algorithmOid === oid); + if (!matchedItem || !matchedItem.parameters) return null; + + return matchedItem.parameters.asn1.contents; +}; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts new file mode 100644 index 000000000..57f0b4d97 --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts @@ -0,0 +1,185 @@ +import { sha256 } from '@noble/hashes/sha256'; + +import { deviceAuthenticityConfig } from './config'; +import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from './verifyAuthenticityProof'; + +import type { DeviceAuthenticityConfig, VerifyAuthenticityProofResult } from './types'; + +export { deviceAuthenticityConfig } from './config'; +export { prepareDeviceAuthenticityData, verifyAuthenticityProof } from './verifyAuthenticityProof'; +export * from './types'; + +/** Raw fields taken from a Trezor `AuthenticityProof` message. */ +export type AuthenticityProof = { + optiga_certificates: string[]; + optiga_signature: string; + tropic_certificates?: string[]; + tropic_signature?: string; + mcu_certificates?: string[]; + mcu_signature?: string; +}; + +export type AuthenticateDeviceResult = { + /** + * True only when EVERY required attestation layer for this model verified up + * to a trusted Trezor root CA AND the device signed our challenge: + * - Optiga (all secure-element models), and + * - Tropic + matching serial number (T3W1 and above). + * ML-DSA / MCU is NOT checked (needs @noble/post-quantum). Only a `true` + * value may be trusted as a genuine, uniquely-identified physical device. + */ + verified: boolean; + /** + * Stable per-device identifier: the X.509 serial number (OID 2.5.4.5) when + * present (T3W1+), otherwise the SHA-256 of the device attestation public key. + * Survives wipe/recovery and cannot be forged from a seed. Do NOT trust when + * `verified` is false — it is then attacker-controlled. + */ + deviceId?: string; + /** Raw device attestation public key (hex) from the Optiga device certificate. */ + deviceCertPubKey?: string; + /** X.509 subject serial number (hex), only present on T3W1 and above. */ + serialNumber?: string; + rootPubKey?: string; + caPubKey?: string; + /** + * True when the chain matched a DEBUG/staging root key (simulator / dev + * device). A production accounting flow must reject verified results that + * have `usedDebugKey === true`. + */ + usedDebugKey?: boolean; + /** Failure reason when `verified` is false. */ + error?: string; +}; + +// Revocation hook: CA public keys (hex) that must be rejected even if otherwise +// valid. Empty today; kept so a leaked intermediate CA can be blocked by config +// rather than a code change. Mirrors @trezor/device-authenticity's blacklist. +export const caPubKeyBlacklist: string[] = []; + +const modelConfigOf = (config: DeviceAuthenticityConfig, deviceModel: string) => { + const modelConfig = config[deviceModel]; + return modelConfig && typeof modelConfig !== 'number' ? modelConfig : undefined; +}; + +const isTropicExpected = ( + config: DeviceAuthenticityConfig, + deviceModel: string, + allowDebugKeys?: boolean, +): boolean => { + const m = modelConfigOf(config, deviceModel); + if (!m) return false; + return ( + (m.rootPubKeysTropic?.length ?? 0) > 0 || + (!!allowDebugKeys && (m.debug?.rootPubKeysTropic?.length ?? 0) > 0) + ); +}; + +const matchedDebugKey = ( + config: DeviceAuthenticityConfig, + deviceModel: string, + rootPubKey?: string, +): boolean => { + if (!rootPubKey) return false; + const m = modelConfigOf(config, deviceModel); + if (!m?.debug) return false; + return [...(m.debug.rootPubKeysOptiga ?? []), ...(m.debug.rootPubKeysTropic ?? [])].includes( + rootPubKey, + ); +}; + +/** + * Verify a Trezor `AuthenticityProof` against a challenge and derive a stable + * per-device id. `challenge` must be the exact random bytes sent in the + * `AuthenticateDevice` request. Never throws — malformed input from a malicious + * device is folded into `{ verified: false }`. + */ +export const authenticateDeviceFromProof = ({ + proof, + challenge, + deviceModel, + config = deviceAuthenticityConfig, + allowDebugKeys, +}: { + proof: AuthenticityProof; + challenge: Buffer; + deviceModel: string; + config?: DeviceAuthenticityConfig; + allowDebugKeys?: boolean; +}): AuthenticateDeviceResult => { + try { + const signedData = prepareDeviceAuthenticityData({ payload: challenge }); + const common = { signedData, deviceModel, config, allowDebugKeys, caPubKeyBlacklist }; + + // 1) Optiga is required on every attestation-capable model. + const { optiga_certificates, optiga_signature } = proof; + if (!optiga_signature || !optiga_certificates?.length) { + return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; + } + const optiga = verifyAuthenticityProof({ + ...common, + certificates: optiga_certificates, + signature: optiga_signature, + }); + if (!optiga.valid) { + return { + verified: false, + deviceCertPubKey: optiga.deviceCertPubKey, + caPubKey: optiga.caPubKey, + rootPubKey: optiga.rootPubKey, + error: optiga.error, + }; + } + + // 2) On T3W1+ the Tropic layer is also required, and its serial number must + // match Optiga's. This defeats a transplanted-Optiga forgery: an attacker + // moving a genuine Optiga onto a fake board cannot also satisfy Tropic. + let usedDebugKey = matchedDebugKey(config, deviceModel, optiga.rootPubKey); + if (isTropicExpected(config, deviceModel, allowDebugKeys)) { + const { tropic_certificates, tropic_signature } = proof; + if (!tropic_signature || !tropic_certificates?.length) { + return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; + } + const tropic = verifyAuthenticityProof({ + ...common, + certificates: tropic_certificates, + signature: tropic_signature, + }); + if (!tropic.valid) { + return { verified: false, error: tropic.error }; + } + const serialsPresent = !!optiga.serialNumber && !!tropic.serialNumber; + if (!serialsPresent || optiga.serialNumber !== tropic.serialNumber) { + return { verified: false, error: 'SERIAL_NUMBER_MISMATCH' }; + } + usedDebugKey = + usedDebugKey || matchedDebugKey(config, deviceModel, tropic.rootPubKey); + } + + const deviceId = + optiga.serialNumber ?? + Buffer.from(sha256(Uint8Array.from(Buffer.from(optiga.deviceCertPubKey, 'hex')))).toString( + 'hex', + ); + + return { + verified: true, + deviceId, + deviceCertPubKey: optiga.deviceCertPubKey, + serialNumber: optiga.serialNumber, + rootPubKey: optiga.rootPubKey, + caPubKey: optiga.caPubKey, + usedDebugKey, + }; + } catch (error) { + // A malicious device can return a malformed certificate that throws deep in + // the parser; never let that escape as a rejected promise. + return { + verified: false, + error: `RESPONSE_MALFORMED: ${(error as Error)?.message ?? 'parse error'}`, + }; + } +}; + +// re-export for callers that want the low-level result shape +export type { VerifyAuthenticityProofResult }; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts new file mode 100644 index 000000000..eaf4c70a9 --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts @@ -0,0 +1,55 @@ +export type CertPubKeys = { + rootPubKeysOptiga: string[]; + rootPubKeysTropic?: string[]; + rootPubKeysMLDSA?: string[]; +}; + +export type ModelPubKeys = CertPubKeys & { + debug?: CertPubKeys; +}; + +// Keyed by Device.features.internal_model (e.g. 'T2B1', 'T3B1', 'T3T1', 'T3W1'). +export interface DeviceAuthenticityConfig { + version: number; + [model: string]: ModelPubKeys | number | undefined; +} + +export type VerifyAuthenticityProofParams = { + certificates: string[]; + signature: string; + signedData: Uint8Array; + /** Device.features.internal_model, selects which root CA keys to trust. */ + deviceModel: string; + config: DeviceAuthenticityConfig; + allowDebugKeys?: boolean; + /** CA public keys (hex) to reject even when otherwise valid (revocation). */ + caPubKeyBlacklist?: string[]; +}; + +export type VerifyAuthenticityProofResult = + | { + valid: true; + caPubKey?: string; + rootPubKey: string; + error?: undefined; + /** X.509 subject serial number (OID 2.5.4.5), present on T3W1 and above. */ + serialNumber?: string; + /** Raw per-device attestation public key from the device certificate (hex). */ + deviceCertPubKey: string; + } + | { + valid: false; + caPubKey?: string; + rootPubKey?: string; + serialNumber?: string; + deviceCertPubKey?: string; + error: + | 'ROOT_PUBKEY_NOT_FOUND' + | 'INVALID_DEVICE_MODEL' + | 'INVALID_DEVICE_CERTIFICATE' + | 'INVALID_DEVICE_SIGNATURE' + | 'CA_PUBKEY_BLACKLISTED' + | 'RESPONSE_PAYLOAD_MISSING' + | 'RESPONSE_MALFORMED' + | 'UNSUPPORTED_ALGORITHM'; + }; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/validateCaCertExtensions.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/validateCaCertExtensions.ts new file mode 100644 index 000000000..1d4b45790 --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/validateCaCertExtensions.ts @@ -0,0 +1,40 @@ +import { type parseCertificate } from './x509certificate'; + +// Check that this certificate is a valid Trezor CA. +// KeyUsage must be present and allow certificate signing. +// BasicConstraints must be present, have the cA flag and a pathLenConstraint. +// Any unrecognized non-critical extension is allowed. Any unrecognized critical extension is disallowed. +export const validateCaCertExtensions = ( + cert: ReturnType, + pathLen: number, +) => { + let hasKeyUsage = false; + let hasBasicConstraints = false; + cert.tbsCertificate.extensions.forEach(ext => { + if (ext.key === 'keyUsage') { + if (ext.keyCertSign !== '1') { + throw new Error(`CA keyCertSign not set`); + } + hasKeyUsage = true; + } else if (ext.key === 'basicConstraints') { + if (!ext.cA) { + throw new Error(`CA basicConstraints.cA not set`); + } + if (typeof ext.pathLenConstraint !== 'number') { + throw new Error('CA basicConstraints.pathLenConstraint not set'); + } + if (ext.pathLenConstraint < pathLen) { + throw new Error('Issuer was not permitted to issue certificate'); + } + hasBasicConstraints = true; + } else if (ext.critical) { + throw new Error(`Unknown critical extension ${ext.key} in CA certificate`); + } + }); + + if (!hasKeyUsage || !hasBasicConstraints) { + throw new Error( + `CA missing extensions keyUsage: ${hasKeyUsage}, basicConstraints: ${hasBasicConstraints}`, + ); + } +}; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts new file mode 100644 index 000000000..57e38298d --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts @@ -0,0 +1,218 @@ +import { findSubjectContentByOid } from './findSubjectContentByOid'; +import { + type DeviceAuthenticityConfig, + type VerifyAuthenticityProofParams, + type VerifyAuthenticityProofResult, +} from './types'; +import { validateCaCertExtensions } from './validateCaCertExtensions'; +import { getVerifyFn } from './verifySignatures'; +import { type ParsedCertificate, parseCertificate } from './x509certificate'; + +const getChunkSize = (byteLength: number): Buffer => { + if (byteLength < 0xfd) return Buffer.from([byteLength]); + if (byteLength <= 0xffff) { + const b = Buffer.alloc(3); + b[0] = 0xfd; + b.writeUInt16LE(byteLength, 1); + + return b; + } + const b = Buffer.alloc(5); + b[0] = 0xfe; + b.writeUInt32LE(byteLength, 1); + + return b; +}; + +// Compose the data against which the signatures are verified: each chunk is +// prefixed with its compact-size length. Matches the firmware's signed payload. +export const prepareDeviceAuthenticityData = ({ + payload, + prefix = 'AuthenticateDevice:', +}: { + payload: Buffer | Buffer[]; + prefix?: string; +}): Uint8Array => { + const chunks = [Buffer.from(prefix), ...(Array.isArray(payload) ? payload : [payload])]; + + return Buffer.concat(chunks.flatMap(chunk => [getChunkSize(chunk.byteLength), chunk])); +}; + +const getRootPubKeys = ({ + config, + deviceModel, + allowDebugKeys, +}: { + config: DeviceAuthenticityConfig; + deviceModel: string; + allowDebugKeys?: boolean; +}): string[] => { + const modelConfig = config[deviceModel]; + if (!modelConfig || typeof modelConfig === 'number') { + throw new Error(`Pubkeys for ${deviceModel} not found in config`); + } + const prod = [ + ...(modelConfig.rootPubKeysOptiga ?? []), + ...(modelConfig.rootPubKeysTropic ?? []), + ]; + if (!allowDebugKeys) return prod; + + return [ + ...prod, + ...(modelConfig.debug?.rootPubKeysOptiga ?? []), + ...(modelConfig.debug?.rootPubKeysTropic ?? []), + ]; +}; + +const matchRootPubKeyToCertificate = ({ + cert, + allRootPubKeys, +}: { + cert: ParsedCertificate; + allRootPubKeys: string[]; +}): string | undefined => { + const verifySignatureFn = getVerifyFn(cert.signatureAlgorithm.algorithmName); + + return allRootPubKeys.find(rootPubKey => + verifySignatureFn( + Buffer.from(rootPubKey, 'hex'), + cert.tbsCertificate.asn1.raw, + cert.signatureValue.bits.bytes, + ), + ); +}; + +// Get internal model name from the device cert Subject (OID 2.5.4.3), e.g. 'T2B1'. +const parseModelFromDeviceCertSubject = (deviceCert: ParsedCertificate) => { + const modelDescriptionBytes = findSubjectContentByOid(deviceCert, '2.5.4.3'); + if (modelDescriptionBytes === null) return null; + + // the whole string is e.g. 'T3W1 Trezor Safe 7', only the first 4 chars are the model + return Buffer.from(modelDescriptionBytes.subarray(0, 4)).toString(); +}; + +// Get serial number from the device cert Subject (OID 2.5.4.5). Only present on T3W1 and above. +const parseSerialNumberFromDeviceCert = (deviceCert: ParsedCertificate) => { + const serialNumber = findSubjectContentByOid(deviceCert, '2.5.4.5'); + if (serialNumber === null) return undefined; + + return Buffer.from(serialNumber).toString('hex'); +}; + +/** + * Verifies the signing scheme: rootPubKey → CA pub key → device key → signature of prefixed challenge. + * Returns the per-device attestation public key (`deviceCertPubKey`) on success. + */ +export const verifyAuthenticityProof = ({ + certificates, + signature, + signedData, + deviceModel, + config, + allowDebugKeys, + caPubKeyBlacklist = [], +}: VerifyAuthenticityProofParams): VerifyAuthenticityProofResult => { + const allRootPubKeys = getRootPubKeys({ config, deviceModel, allowDebugKeys }); + + const parsedCertificates = certificates.map(c => + parseCertificate(new Uint8Array(Buffer.from(c, 'hex'))), + ); + + const firstCertAlgName = parsedCertificates[0]?.signatureAlgorithm.algorithmName; + // Only the [device, CA] scheme (P-256 / Ed25519) is supported. The single-cert + // ML-DSA scheme (T3W1 secondary) is not verifiable here. + if (firstCertAlgName !== 'P-256' && firstCertAlgName !== 'Ed25519') { + return { + valid: false, + error: firstCertAlgName === 'MLDSA44' ? 'UNSUPPORTED_ALGORITHM' : 'RESPONSE_MALFORMED', + }; + } + if (parsedCertificates.length !== 2) { + return { valid: false, error: 'RESPONSE_MALFORMED' }; + } + + const [deviceCert, caCert] = parsedCertificates; + const deviceCertPubKey = Buffer.from( + deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes, + ).toString('hex'); + + const deviceCertAlgName = deviceCert.signatureAlgorithm.algorithmName; + const caCertAlgName = caCert.signatureAlgorithm.algorithmName; + if (deviceCertAlgName !== caCertAlgName) { + return { valid: false, deviceCertPubKey, error: 'RESPONSE_MALFORMED' }; + } + + validateCaCertExtensions(caCert, 0); // pathLenConstraint is always 0 (single CA cert, no chain) + const verifySignatureFn = getVerifyFn(deviceCertAlgName); + + // 1) CA certificate must be signed by one of the trusted root pub keys. + const caPubKeyBytes = caCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes; + const caPubKey = Buffer.from(caPubKeyBytes).toString('hex'); + const rootPubKeyMatch = matchRootPubKeyToCertificate({ allRootPubKeys, cert: caCert }); + if (rootPubKeyMatch === undefined) { + return { valid: false, caPubKey, deviceCertPubKey, error: 'ROOT_PUBKEY_NOT_FOUND' }; + } + // Revocation: reject a specific (e.g. leaked) intermediate CA even if it chains + // to a trusted root. + if (caPubKeyBlacklist.includes(caPubKey)) { + return { + valid: false, + caPubKey, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'CA_PUBKEY_BLACKLISTED', + }; + } + + // 2) Device model in the certificate must match the connected device. + const modelFromSubject = parseModelFromDeviceCertSubject(deviceCert); + if (modelFromSubject !== deviceModel) { + return { + valid: false, + caPubKey, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'INVALID_DEVICE_MODEL', + }; + } + + // 3) Device certificate must be signed by the CA pub key. + const isDeviceCertValid = verifySignatureFn( + Buffer.from(caPubKeyBytes), + deviceCert.tbsCertificate.asn1.raw, + deviceCert.signatureValue.bits.bytes, + ); + if (!isDeviceCertValid) { + return { + valid: false, + caPubKey, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'INVALID_DEVICE_CERTIFICATE', + }; + } + + // 4) The challenge signature must be produced by the device key. + const isSignatureValid = verifySignatureFn( + Buffer.from(deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes), + signedData, + Buffer.from(signature, 'hex'), + ); + if (!isSignatureValid) { + return { + valid: false, + caPubKey, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'INVALID_DEVICE_SIGNATURE', + }; + } + + return { + valid: true, + caPubKey, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + serialNumber: parseSerialNumberFromDeviceCert(deviceCert), + }; +}; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts new file mode 100644 index 000000000..afa7ccd33 --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts @@ -0,0 +1,44 @@ +import { ed25519 } from '@noble/curves/ed25519'; +import { p256 } from '@noble/curves/p256'; +import { sha256 } from '@noble/hashes/sha256'; + +import { type AlgorithmName, fixSignature } from './x509certificate'; + +export type VerifySignature = ( + rawKey: Uint8Array, + data: Uint8Array, + signature: Uint8Array, +) => boolean; + +// P-256 (secp256r1) ECDSA over SHA-256. Hardware attestation signatures are NOT +// guaranteed to be low-S normalized, so lowS must be disabled or valid signatures +// would be rejected. `fixSignature` repairs the 1-in-256 malformed Optiga DER encoding. +export const verifySignatureP256: VerifySignature = (rawKey, data, signature) => { + try { + const sig = p256.Signature.fromDER(Buffer.from(fixSignature(signature)).toString('hex')); + + return p256.verify(sig.toBytes('compact'), sha256(data), rawKey, { lowS: false }); + } catch { + // invalid inputs (e.g. an Ed25519 signature with a P-256 key) are an + // unsuccessful verification, not a runtime error + return false; + } +}; + +export const verifySignatureEd25519: VerifySignature = (rawKey, data, signature) => { + try { + return ed25519.verify(signature, data, rawKey); + } catch { + return false; + } +}; + +export const getVerifyFn = (algorithmName: AlgorithmName): VerifySignature => { + if (algorithmName === 'P-256') return verifySignatureP256; + if (algorithmName === 'Ed25519') return verifySignatureEd25519; + // MLDSA44 is a T3W1-only secondary (Tropic/MCU) signature and needs + // @noble/post-quantum, which is not a dependency here. The primary Optiga + // (P-256) result already yields a valid per-device identity for every + // attestation-capable model, so ML-DSA verification is intentionally unsupported. + throw new Error(`Unsupported signature algorithm: ${algorithmName}`); +}; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/x509certificate.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/x509certificate.ts new file mode 100644 index 000000000..74772cc1b --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/x509certificate.ts @@ -0,0 +1,473 @@ +/** + * Parse an x509 certificate returned from Trezor (AuthenticityProof) from DER format. + * Ported from @trezor/device-authenticity (self-contained, no external deps). + * inspired by https://blog.engelke.com/2014/10/21/web-crypto-and-x-509-certificates/ + */ + +interface Asn1 { + cls: number; // ASN.1 class of the object. + tag: number; // ASN.1 object type. + structured: boolean; // Structured objects are encoded ASN.1 objects. Other objects (primitive) values depends on the tag and class + byteLength: number; // value size + contents: Uint8Array; // byte array containing the object value. + raw: Uint8Array; // original byte array +} + +type OneOrTwoNumbers = `${number}` | `${number}.${number}`; +// type that allows between 4 to 8 numbers to specify an OID +export type Oid = `${OneOrTwoNumbers}.${OneOrTwoNumbers}.${OneOrTwoNumbers}.${OneOrTwoNumbers}`; + +// algorithms supported to verify certificates and signatures +export type AlgorithmName = 'P-256' | 'Ed25519' | 'MLDSA44' | 'unknown'; + +type Extension = + | { + key: 'keyUsage'; + critical?: boolean; + keyCertSign: '0' | '1'; + } + | { + key: 'basicConstraints'; + critical?: boolean; + cA: boolean; + pathLenConstraint?: number; + } + | (Asn1 & { + key: Oid; + critical?: boolean; + }); + +const parseOidToAlgorithmName = (oid: Oid): AlgorithmName => { + if (oid === '1.2.840.10045.4.3.2') return 'P-256'; // https://oid-base.com/get/1.2.840.10045.4.3.2 + if (oid === '1.3.101.112') return 'Ed25519'; // https://oid-base.com/get/1.3.101.112 + if (oid === '2.16.840.1.101.3.4.3.17') return 'MLDSA44'; // https://oid-base.com/get/2.16.840.1.101.3.4.3.17 + + return 'unknown'; +}; + +const derToAsn1 = (byteArray: Uint8Array): Asn1 => { + let position = 0; + + function getTag() { + let tag = byteArray[0] & 0x1f; + position += 1; + if (tag === 0x1f) { + tag = 0; + while (byteArray[position] >= 0x80) { + tag = tag * 128 + byteArray[position] - 0x80; + position += 1; + } + tag = tag * 128 + byteArray[position] - 0x80; + position += 1; + } + + return tag; + } + + function getLength() { + let length = 0; + + if (byteArray[position] < 0x80) { + length = byteArray[position]; + position += 1; + } else { + const numberOfDigits = byteArray[position] & 0x7f; + position += 1; + length = 0; + for (let i = 0; i < numberOfDigits; i++) { + length = length * 256 + byteArray[position]; + position += 1; + } + } + + return length; + } + + const cls = (byteArray[0] & 0xc0) / 64; + const structured = (byteArray[0] & 0x20) === 0x20; + const tag = getTag(); + + if (byteArray[position] === 0x80) { + // DER forbids indefinite length encoding. You must use the definite length encoding (that is, with the length specified at the beginning). + // https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ + throw new Error('Unsupported length encoding'); + } + + const length = getLength(); // As encoded, which may be special value 0 + const byteLength = position + length; + const contents = byteArray.subarray(position, byteLength); + + const raw = byteArray.subarray(0, byteLength); // May not be the whole input array + + return { + cls, + tag, + structured, + byteLength, + contents, + raw, + }; +}; + +const derToAsn1List = (byteArray: Uint8Array) => { + const result = []; + let nextPosition = 0; + while (nextPosition < byteArray.length) { + const nextPiece = derToAsn1(byteArray.subarray(nextPosition)); + result.push(nextPiece); + nextPosition += nextPiece.byteLength; + } + + return result; +}; + +const derBitStringValue = (byteArray: Uint8Array) => ({ + unusedBits: byteArray[0], + bytes: byteArray.subarray(1), +}); + +const parseSignatureValue = (asn1: Asn1) => { + if (asn1.cls !== 0 || asn1.tag !== 3 || asn1.structured) { + throw new Error('Bad signature value. Not a BIT STRING.'); + } + + return { + asn1, + bits: derBitStringValue(asn1.contents), + }; +}; + +// Optiga may produce a malformed signature with probability 1 in 256. +// https://github.com/trezor/trezor-firmware/issues/3411 +export const fixSignature = (byteArray: Uint8Array) => { + const asn1 = derToAsn1(byteArray); + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error('Bad signature. Not a SEQUENCE.'); + } + + const items = derToAsn1List(asn1.contents); + let newLength = 0; + const fixedItems = items.map(chunk => { + // find first significant byte + const index = chunk.contents.findIndex(value => value > 0x00); + const data = chunk.contents.subarray(index); + // According to the DER-encoding rules, the integers are supposed to be prefixed with a 0x00 byte + // if **and only if** the most significant byte is >= 0x80 + const offset = data[0] >= 0x80 ? 1 : 0; + // create replacement for chunk + const chunkLength = data.length + offset; + const newChunk = new Uint8Array(chunkLength + 2); + // set first two bytes: original value and new length of the chunk + newChunk.set([chunk.raw[0], chunkLength]); + // optionally add 0 + if (offset > 0) { + newChunk.set([0], 2); + } + // fill new chunk with data + newChunk.set(data, 2 + offset); + newLength += newChunk.length; + + return newChunk; + }); + + // create replacement for sequence object + const signature = new Uint8Array(newLength + 2); + // set two first bytes: original value and new length of all chunks + signature.set([byteArray[0], newLength]); + // fill new sequence with fixed items + let signatureOffset = 2; + fixedItems.forEach(item => { + signature.set(item, signatureOffset); + + signatureOffset += item.length; + }); + + return signature; +}; + +const derObjectIdentifierValue = (byteArray: Uint8Array) => { + let oid = `${Math.floor(byteArray[0] / 40)}.${byteArray[0] % 40}`; + let position = 1; + while (position < byteArray.length) { + let nextInteger = 0; + while (byteArray[position] >= 0x80) { + nextInteger = nextInteger * 0x80 + (byteArray[position] & 0x7f); + position += 1; + } + nextInteger = nextInteger * 0x80 + byteArray[position]; + position += 1; + oid += `.${nextInteger}`; + } + + return oid as Oid; +}; + +/* + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + */ +const parseAlgorithmIdentifier = (asn1: Asn1) => { + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error('Bad algorithm identifier. Not a SEQUENCE.'); + } + const pieces = derToAsn1List(asn1.contents); + if (pieces.length > 2) { + throw new Error('Bad algorithm identifier. Contains too many child objects.'); + } + const encodedAlgorithm = pieces[0]; + if (encodedAlgorithm.cls !== 0 || encodedAlgorithm.tag !== 6 || encodedAlgorithm.structured) { + throw new Error('Bad algorithm identifier. Does not begin with an OBJECT IDENTIFIER.'); + } + const algorithmOid = derObjectIdentifierValue(encodedAlgorithm.contents); + const algorithmName = parseOidToAlgorithmName(algorithmOid); + + return { + asn1, + algorithmOid, + algorithmName, + parameters: pieces.length === 2 ? { asn1: pieces[1] } : null, + }; +}; + +/* + * Name ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + */ +export const parseName = (asn1: Asn1) => + // SEQUENCE > SET > SEQUENCE + derToAsn1List(asn1.contents).map(item => { + const attrSet = derToAsn1(item.contents); + + return parseAlgorithmIdentifier(attrSet); + }); + +/* + * SubjectPublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * subjectPublicKey BIT STRING + * } + */ +const parseSubjectPublicKeyInfo = (asn1: Asn1) => { + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error('Bad SPKI. Not a SEQUENCE.'); + } + const pieces = derToAsn1List(asn1.contents); + if (pieces.length !== 2) { + throw new Error('Bad SubjectPublicKeyInfo. Wrong number of child objects.'); + } + + return { + asn1, + algorithm: parseAlgorithmIdentifier(pieces[0]), + bits: derBitStringValue(pieces[1].contents), + }; +}; + +/* + * Time ::= CHOICE { + * utcTime UTCTime, + * generalTime GeneralizedTime + * } + * NOTE: UTC and generalized times may both appear. (generalized > 2050) + */ +const parseUtcTime = (time: Asn1) => { + // tag: 23 = UTCTime (YYMMDDhhmmZ); + // tag: 24 = GeneralizedTime (YYYYMMDDhhmmZ) + let offset = 4; + let yearOffset = 0; + if (time.tag === 23) { + offset = 2; + yearOffset = 2000; + } + const utc = Buffer.from(time.contents).toString(); + const year = yearOffset + Number(utc.substring(0, offset)); + const month = Number(utc.substring(offset, offset + 2)) - 1; + const day = Number(utc.substring(offset + 2, offset + 4)); + const hour = Number(utc.substring(offset + 4, offset + 6)); + const minute = Number(utc.substring(offset + 6, offset + 8)); + + const date = new Date(); + date.setUTCFullYear(year, month, day); + date.setUTCHours(hour, minute, 0); + + return date; +}; + +/* + * Validity ::= SEQUENCE { + * from Time, + * to Time + * } + */ +const parseValidity = (asn1: Asn1) => { + const [from, to] = derToAsn1List(asn1.contents); + + return { + from: parseUtcTime(from), + to: parseUtcTime(to), + }; +}; + +/* + * https://www.alvestrand.no/objectid/2.5.29.html + * Extensions ::= SEQUENCE { + * extnID OBJECT IDENTIFIER, + * critical BOOLEAN OPTIONAL, + * extnValue OCTET STRING + * } + */ +const parseExtensions = (data: Asn1) => { + const asn1 = derToAsn1(data.contents); + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error("This can't be a Extension. Wrong data type."); + } + + const readBoolean = (value?: Asn1) => { + if (!value) return false; + if (value.cls !== 0 || value.tag !== 1 || value.contents.length !== 1 || value.structured) { + throw new Error("This can't be a boolean. Wrong data type."); + } + if (![0x00, 0xff].includes(value.contents[0])) { + throw new Error('Invalid boolean value.'); + } + + return value.contents[0] === 0xff; + }; + + const readBitString = (uint8Array: Uint8Array) => { + const buffer = Buffer.from(uint8Array); + const tag = buffer.readUInt8(0); + if (tag !== 3) { + throw new Error("This can't be a bit string. Wrong data type."); + } + const length = buffer.readUInt8(1); + const unusedBits = buffer.readUInt8(2); + const bitStringBytes = buffer.subarray(3, 3 + length - 1); // -1 because the length includes the unusedBits byte + const bitString = bitStringBytes.reduce( + (str, byte) => str + byte.toString(2).padStart(8, '0'), + '', + ); + + return bitString.slice(0, bitString.length - unusedBits) as '01'; + }; + + const readInteger = (value?: Asn1) => { + if (!value) return undefined; + if (value.cls !== 0 || value.tag !== 2 || value.contents.length !== 1 || value.structured) { + throw new Error("This can't be a integer. Wrong data type."); + } + + return Buffer.from(value.contents).readInt8(); + }; + + const extensions: Extension[] = []; + derToAsn1List(asn1.contents).forEach(item => { + const [id, ...pieces] = derToAsn1List(item.contents); + if (id.cls !== 0 || id.tag !== 6 || id.structured) { + throw new Error('Bad extension. Does not begin with an OBJECT IDENTIFIER.'); + } + + const algorithm = derObjectIdentifierValue(id.contents); + const critical = pieces.length > 1 ? readBoolean(pieces[0]) : false; + const extnValue = pieces.length > 1 ? pieces[1] : pieces[0]; + if (extnValue.cls !== 0 || extnValue.tag !== 4 || extnValue.structured) { + throw new Error("This can't be a octet string. Wrong data type."); + } + + if (algorithm === '2.5.29.15') { + // https://www.alvestrand.no/objectid/2.5.29.15.html + extensions.push({ + key: 'keyUsage', + critical, + keyCertSign: readBitString(extnValue.contents)[5] as '0' | '1', + }); + } else if (algorithm === '2.5.29.19') { + // https://www.alvestrand.no/objectid/2.5.29.19.html + const fields = derToAsn1List(derToAsn1(extnValue.contents).contents); + const ca = fields.length > 0 && fields[0].tag === 1 ? fields[0] : undefined; + const len = fields.length > 0 && fields[0].tag === 2 ? fields[0] : fields[1]; + + extensions.push({ + key: 'basicConstraints', + critical, + cA: readBoolean(ca), + pathLenConstraint: readInteger(len), + }); + } else { + extensions.push({ + key: algorithm, + critical, + ...item, + }); + } + }); + + return extensions; +}; + +/* + * TBSCertificate ::= SEQUENCE { + * version [0] EXPLICIT Version DEFAULT v1, + * serialNumber CertificateSerialNumber, + * signature AlgorithmIdentifier, + * issuer Name[], + * validity Validity, + * subject Name[], + * subjectPublicKeyInfo SubjectPublicKeyInfo, + * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + * extensions [3] EXPLICIT Extensions OPTIONAL + * } + */ +const parseTBSCertificate = (asn1: Asn1) => { + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error("This can't be a TBSCertificate. Wrong data type."); + } + const pieces = derToAsn1List(asn1.contents); + if (pieces.length < 7) { + throw new Error('Bad TBS Certificate. There are fewer than the seven required children.'); + } + + return { + asn1, + version: pieces[0], + serialNumber: pieces[1], + signature: parseAlgorithmIdentifier(pieces[2]), + issuer: pieces[3], + validity: parseValidity(pieces[4]), + subject: parseName(pieces[5]), + subjectPublicKeyInfo: parseSubjectPublicKeyInfo(pieces[6]), + extensions: parseExtensions(pieces[7]), + }; +}; + +/* + * Certificate ::= SEQUENCE { + * tbsCertificate TBSCertificate, + * signatureAlgorithm AlgorithmIdentifier, + * signatureValue BIT STRING + * } + */ +export const parseCertificate = (byteArray: Uint8Array) => { + const asn1 = derToAsn1(byteArray); + if (asn1.cls !== 0 || asn1.tag !== 16 || !asn1.structured) { + throw new Error("This can't be an X.509 certificate. Wrong data type."); + } + const pieces = derToAsn1List(asn1.contents); + if (pieces.length !== 3) { + throw new Error('Certificate contains more than the three specified children.'); + } + + return { + asn1, + tbsCertificate: parseTBSCertificate(pieces[0]), + signatureAlgorithm: parseAlgorithmIdentifier(pieces[1]), + signatureValue: parseSignatureValue(pieces[2]), + }; +}; + +export type ParsedCertificate = ReturnType; diff --git a/packages/hwk-trezor-connector/src/index.ts b/packages/hwk-trezor-connector/src/index.ts index 3f575d9ad..964eda576 100644 --- a/packages/hwk-trezor-connector/src/index.ts +++ b/packages/hwk-trezor-connector/src/index.ts @@ -437,6 +437,15 @@ export abstract class TrezorConnectorBase implements IConnector { ); case 'wipeDevice': return getTrezorResponseMessage(await session.deviceSession.call('WipeDevice', {})); + case 'authenticateDevice': { + // Device attestation: the device signs the host challenge with its + // secure-element (Optiga) key and returns its certificate chain. Requires + // an on-device confirmation and a secure-element model (Safe 3/5/T3W1). + const p = (params ?? {}) as { challenge: string }; + return getTrezorResponseMessage( + await session.deviceSession.call('AuthenticateDevice', { challenge: p.challenge }) + ); + } // --- THP application-session control (Trezor-specific, driven by the // adapter to align the active passphrase session before a chain call). // The connector deals only in session ids; wallet identity belongs to the diff --git a/yarn.lock b/yarn.lock index 4874c5cfb..e7075eaf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4972,7 +4972,7 @@ resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== -"@noble/curves@1.2.0", "@noble/curves@1.9.7", "@noble/curves@^1.1.0", "@noble/curves@^1.3.0", "@noble/curves@^1.4.2", "@noble/curves@~1.4.0": +"@noble/curves@1.2.0", "@noble/curves@1.9.7", "@noble/curves@^1.1.0", "@noble/curves@^1.3.0", "@noble/curves@^1.4.2", "@noble/curves@^1.9.7", "@noble/curves@~1.4.0": version "1.9.7" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== From 171686d32036e27db52835dfe59ed2db4ab65af1 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Sun, 26 Jul 2026 09:43:19 +0800 Subject: [PATCH 02/12] feat: add third-party device voucher proofs --- HANDOFF-device-attestation.md | 51 ++- docs/device-attestation-voucher-backend.md | 424 ++++++++++++++++++ packages/hwk-adapter-core/src/index.ts | 3 + .../hwk-adapter-core/src/types/connector.ts | 28 ++ packages/hwk-adapter-core/src/types/wallet.ts | 56 +++ .../src/__tests__/LedgerAdapter.test.ts | 68 +++ .../src/__tests__/LedgerConnectorBase.test.ts | 63 +++ .../src/adapter/LedgerAdapter.ts | 42 +- .../src/connector/LedgerConnectorBase.ts | 107 +++-- .../src/__tests__/TrezorAdapter.test.ts | 66 +++ .../src/adapter/TrezorAdapter.ts | 53 ++- .../__tests__/verifyAuthenticityProof.test.ts | 15 +- .../src/deviceAuthenticity/index.ts | 63 ++- 13 files changed, 936 insertions(+), 103 deletions(-) create mode 100644 docs/device-attestation-voucher-backend.md diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index 56e5bc3b5..e83984971 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -201,7 +201,54 @@ OneKey 侧已有用户账户地址/xpub。对每条导入 `{地址或xpub → --- ## 8. 未提交 / 未决事项 -- **两个仓库的改动都未 commit**(本会话没提交)。 +- 原始调试工作已分别 checkpoint;生产接入工作在 + `codex/device-attestation-voucher-proof` 和 + `codex/third-party-device-onboarding-rewards` 分支继续。 - 设备 ID:Ledger `attestationPubKey` 字段 + 两台同批次 deviceId 唯一性待真机确认。 - 分账合规提醒:采集第三方设备 ID 属设备指纹,多辖区算个人数据需披露同意;attestation 只证明"真设备",不证明"同一个人"。 -- 账户名迁移:范围未定(见 6.4)。 +- Trezor Suite 云端标签仍需要 Dropbox/Google/suite-sync 的产品授权范围;App 会明确返回 + `cloud_source_requires_authorization`,不会误报成已同步。 + +--- + +## 9. 生产接入实现(2026-07-26) + +后端协议、安全边界、表结构、幂等事务、Ledger relay 及上线测试门槛已整理到: + +- `docs/device-attestation-voucher-backend.md` + +SDK: + +- `IHardwareWallet.verifyDeviceAuthenticity(connectId, params)` 成为公共类型; +- Trezor 接受服务端 32-byte challenge,原样调用 `AuthenticateDevice`,返回 raw + certificates/signatures 给后端独立验证; +- Ledger connector 支持单次 `wss://` genuine-check relay base,运行结束恢复 Ledger + 官方默认; +- bridged/combined connector 支持运行时 `configure`; +- Ledger `isGenuine=false` 的结果不再返回 attacker-controlled device identity; +- reward 客户端不使用 `dangerouslyAllowDebugKeys`。 + +App: + +- 仅 `FinalizeWalletSetup` 识别到真正新增的 Ledger/Trezor wallet 时,在用户点 + Enter wallet 后运行 post-add flow; +- Electron main 读取 Ledger Live `app.json`,只向 background 返回 + Ethereum `name/address` 最小字段;`xpub`、源 account id 与完整账户对象不跨 IPC; + 支持 `accountNames` tuple 和旧结构,并对 password-encrypted 数据 fail closed; +- 相同 EVM 地址且所有候选名称一致时,Ledger Live 名称先经用户确认,再调用 + `setAccountName`;读取、确认和 rename RPC 均有尾超时; +- 之后显示设备奖励弹窗:服务端 challenge → 物理设备证明 → canonical address + signature → 服务端 claim; +- Trezor claim 上传 raw proof;Ledger claim 只上传后端拥有的 relay session id,不上传 + 客户端 `verified/deviceId` 作为证据; +- relay URL 只允许当前环境 OneKey attestation origin、固定 path 与单次 token,穿过 + SW/offscreen bridge 时不会写入 SDK 日志; +- claim background method 显式重建白名单 DTO,运行时额外字段(包括客户端 + `verified/deviceId/DSID`)不会透传; +- 任何名称迁移、厂商服务或 reward API 失败都不阻止钱包创建和进入首页。 + +服务端上线 gate: + +- App 的“新 wallet id”判断只控制 UI,`walletAddAttemptId/walletId` 不证明首次添加; +- 发券必须由服务端根据可信 creation event,或 OneKey ID + 已验证 DSID + 地址 + + campaign 的历史 claim 判定资格;后端未实现前不得开放活动。 diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md new file mode 100644 index 000000000..931f03fa3 --- /dev/null +++ b/docs/device-attestation-voucher-backend.md @@ -0,0 +1,424 @@ +# 第三方硬件钱包首次添加、设备认证与优惠券绑定 + +## 1. 目标与不可违反的边界 + +本方案只在用户**首次创建一个新的 Trezor 或 Ledger 钱包**后触发,不在导入已存在钱包、重新连接设备、普通签名或 App 启动时重复弹出。 + +流程包含两个互不阻塞的可选步骤: + +1. 从厂商客户端读取能够与新钱包账户匹配的本地名称,经用户确认后同步到 OneKey。 +2. 用户主动领取活动优惠券。领取必须同时证明: + - 当前账户地址的控制权; + - 当前连接的是通过厂商认证的真实物理设备; + - 设备认证来自本次服务端创建、未过期且未使用的 challenge。 + +以下数据**不能单独作为发券依据**: + +- 客户端提交的 `verified: true`; +- 客户端提交的 DSID / `deviceId`; +- 钱包地址签名; +- Ledger/Trezor 的普通设备特征、序列号或 USB/BLE 标识。 + +地址签名只证明当前钱包种子能够控制该地址,同一助记词可以出现在软件钱包或另一台硬件钱包中,不能证明物理设备身份。 + +## 2. 总体流程 + +```mermaid +sequenceDiagram + participant UI as OneKey UI + participant BG as OneKey Background + participant DEV as Trezor / Ledger + participant API as OneKey Attestation API + participant VENDOR as Vendor Attestation Service + participant DB as Reward DB + + UI->>BG: 首次创建第三方钱包完成 + BG-->>UI: 可匹配的账户名称 + UI->>BG: 用户确认同步名称 + BG->>BG: setAccountName + + UI->>API: POST /v1/device-rewards/challenges + API->>DB: 创建一次性 challenge + API-->>UI: challengeId, nonce, vendor relay config + + alt Trezor + UI->>DEV: AuthenticateDevice(nonce) + DEV-->>UI: certificates + signatures + UI->>API: raw Trezor proof + address signature + API->>API: 固定生产根证书验证 proof + else Ledger + UI->>DEV: genuine check via OneKey relay URL + DEV->>API: DMK secure-channel WebSocket + API->>VENDOR: 透明转发 genuine session + VENDOR-->>API: genuine verdict + API->>DB: 将设备证书、verdict 绑定 relay session + UI->>API: relay session + address signature + end + + API->>DB: 原子消费 challenge + 去重 + 发券 + API-->>UI: voucher issued / already claimed / rejected +``` + +## 3. API + +所有接口都要求 OneKey 登录态。后端从登录态取得 `userId`,不得接受客户端自行传入 `userId`。 + +### 3.1 创建 challenge + +`POST /v1/device-rewards/challenges` + +请求: + +```json +{ + "version": 1, + "vendor": "trezor", + "campaignId": "third-party-hardware-2026", + "walletAddAttemptId": "opaque-client-attempt-id", + "walletId": "hw-...", + "accountAddress": "0x...", + "networkId": "evm--1" +} +``` + +响应: + +```json +{ + "version": 1, + "challengeId": "opaque-server-id", + "challengeHex": "64 lowercase hex characters", + "expiresAt": 1785000000000, + "audience": "onekey-device-voucher", + "purpose": "first-wallet-add", + "addressMessage": { + "version": 1, + "challengeId": "opaque-server-id", + "campaignId": "third-party-hardware-2026", + "walletAddAttemptId": "opaque-client-attempt-id", + "accountAddress": "canonical address", + "networkId": "evm--1", + "expiresAt": 1785000000000, + "audience": "onekey-device-voucher", + "purpose": "first-wallet-add" + }, + "ledgerRelay": null +} +``` + +Ledger 响应的 `ledgerRelay` 为: + +```json +{ + "attestationSessionId": "opaque-relay-session-id", + "webSocketUrl": "wss://attestation.onekeycn.com/v1/ledger/session/" +} +``` + +要求: + +- `challengeHex` 使用 CSPRNG 生成 32 bytes; +- challenge TTL 建议 5 分钟; +- `walletAddAttemptId` 只用于幂等和关联,不能证明这是首次添加; +- 服务端必须根据账户/钱包历史或可信的首次添加事件判断活动资格; +- 同一用户、设备、活动的并发 challenge 数量要限流; +- `webSocketUrl` 内的 token 不写日志、不进埋点、不持久化到客户端配置。 +- App background 只接受当前环境对应的 + `wss://attestation./v1/ledger/session/`,拒绝任意 + WSS origin、query、fragment、userinfo 和非预期 path;生产为 + `attestation.onekeycn.com`,测试为 `attestation.onekeytest.com`。 + +### 3.1.1 “首次添加”的权威判定 + +App 的 `FinalizeWalletSetup` 仅负责“只在本次确实生成了新 wallet id 时展示 UI”,这个 +本地条件不能成为服务端资格证明。现有第三方钱包流程也不会写入旧的 OneKey hardware +creation-record API。因此: + +- `walletAddAttemptId`、`walletId` 和客户端时间戳都只能用于关联/幂等; +- 服务端必须使用已登录 OneKey ID、campaign、规范化地址、最终验证出的 vendor DSID + 和历史 claim 记录判定“首次”; +- 如果业务要求的是“服务端已知的首次创建事件”,必须由可信账户同步/注册服务先写入 + append-only creation event,并由 challenge 服务读取;不能新增一个由客户端任意调用的 + `mark-created` 接口; +- 在可信 creation event 尚未接通时,资格应降级为“该用户、设备、地址、campaign + 从未成功领取”,或直接返回 `not_eligible`,不得把客户端的首次弹窗当依据。 + +### 3.2 提交 claim + +`POST /v1/device-rewards/claims` + +共同字段: + +```json +{ + "version": 1, + "challengeId": "opaque-server-id", + "inviteCode": "optional-code", + "addressSignature": { + "scheme": "evm-personal-sign", + "address": "0x...", + "signature": "0x..." + } +} +``` + +Trezor evidence: + +```json +{ + "vendor": "trezor", + "scheme": "trezor-authenticate-device-v1", + "deviceModelHint": "T3W1", + "proof": { + "optiga_certificates": ["hex DER"], + "optiga_signature": "hex", + "tropic_certificates": ["hex DER"], + "tropic_signature": "hex", + "mcu_certificates": ["hex DER"], + "mcu_signature": "hex" + } +} +``` + +Ledger evidence: + +```json +{ + "vendor": "ledger", + "scheme": "ledger-genuine-relay-v1", + "attestationSessionId": "opaque-relay-session-id" +} +``` + +Ledger 客户端提交的 `verified`、`deviceId`、`attestationPubKey` 只能用于诊断,不出现在 claim contract 中。权威 verdict 和 DSID 必须来自后端保存的 relay session。 + +成功响应: + +```json +{ + "status": "issued", + "claimId": "opaque-claim-id", + "voucher": { + "campaignId": "third-party-hardware-2026", + "code": "server-issued-code", + "expiresAt": 1789000000000 + } +} +``` + +业务状态使用稳定枚举: + +- `issued` +- `already_claimed` +- `challenge_expired` +- `challenge_consumed` +- `address_signature_invalid` +- `device_proof_invalid` +- `device_not_genuine` +- `ledger_session_incomplete` +- `not_eligible` +- `campaign_unavailable` + +认证失败使用正常业务响应或明确的 4xx,不把证书解析错误、上游响应内容或内部 DSID 泄露给客户端。 + +## 4. 服务端验证 + +### 4.1 规范化地址消息 + +后端返回完整 `addressMessage` 对象,客户端使用项目的 `stableStringify` 后签名。后端必须用完全相同的字段顺序、UTF-8 编码和签名 scheme 重新构造消息,不能验证客户端上传的任意 message。 + +地址签名至少绑定: + +- `challengeId` +- `campaignId` +- `walletAddAttemptId` +- `accountAddress` +- `networkId` +- `expiresAt` +- `audience` +- `purpose` + +后端验证签名恢复出的地址、challenge 内保存的地址和 claim 地址三者一致。UTXO、Solana 等链应使用各自明确的签名 scheme,不能把 EVM 规则泛化。 + +### 4.2 Trezor + +1. 从数据库读取 challenge 原始 32-byte nonce; +2. 限制每个证书、证书链和请求体大小,拒绝畸形或超大 DER; +3. 使用后端内置、版本化的生产 root 集合及吊销表; +4. 验证证书链角色、有效期、签名算法与 nonce 签名; +5. 从证书推导型号,客户端的 `deviceModelHint` 只做一致性检查; +6. T3W1 强制 Optiga + Tropic,两条链的 subject serial 必须一致; +7. 不接受 debug/staging root; +8. 从已验证证书推导权威 vendor DSID: + - 有 subject serial:`trezor:v1:`; + - 否则:`trezor:v1:`。 + +当前 SDK 的本地验证用于即时 UX;后端必须独立验证同一份 raw proof。生产发券代码不得使用 `dangerouslyAllowDebugKeys`。 + +### 4.3 Ledger + +Ledger DMK genuine check 的 secure channel 位于设备与 Ledger HSM 之间,公开 action 只给出 genuine verdict,不能把客户端布尔值转换成 OneKey 可验证证明。因此: + +1. 后端创建一次性 relay session 和单次 WSS token; +2. relay 只允许既定的 Ledger genuine 上游,禁止任意代理; +3. relay 在不改变字节流的前提下记录本次会话所需的协议状态; +4. 必须观察到同一会话的设备 attestation certificate 读取和最终 genuine verdict; +5. 设备公钥与 verdict 必须属于同一上游连接、同一 relay session; +6. 会话完成后立刻失效,不允许重连或重放; +7. 权威 DSID 为 `ledger:v1:`; +8. 上游断开、协议缺失、无法确定 freshness 或 `isGenuine=false` 时一律拒绝发券。 + +relay 方案上线前必须完成 Ledger 协议、服务条款、freshness 和重放保证的安全评审。如果 relay 不能可靠建立这些保证,Ledger 活动应保持 fail closed,直到 Ledger 提供绑定 OneKey nonce/audience 的 JWS/COSE receipt,或使用具备 endorsement attestation 的专用 Ledger App。 + +## 5. 数据模型与事务 + +建议表: + +```sql +create table device_reward_challenge ( + id uuid primary key, + user_id text not null, + vendor text not null, + campaign_id text not null, + wallet_add_attempt_id text not null, + wallet_id_hash bytea not null, + account_address text not null, + network_id text not null, + nonce_ciphertext bytea not null, + expires_at timestamptz not null, + consumed_at timestamptz, + created_at timestamptz not null +); + +create unique index device_reward_challenge_attempt_uq + on device_reward_challenge(user_id, campaign_id, wallet_add_attempt_id); + +create table device_attestation_session ( + id uuid primary key, + challenge_id uuid not null references device_reward_challenge(id), + vendor text not null, + state text not null, + vendor_dsid_ciphertext bytea, + claim_key bytea, + assurance text, + expires_at timestamptz not null, + completed_at timestamptz +); + +create table device_reward_claim ( + id uuid primary key, + challenge_id uuid not null unique references device_reward_challenge(id), + user_id text not null, + campaign_id text not null, + claim_key bytea not null, + address_claim_key bytea not null, + voucher_id text not null unique, + created_at timestamptz not null, + unique(campaign_id, claim_key), + unique(campaign_id, address_claim_key) +); +``` + +`claimKey = HMAC-SHA256(serverKey, vendorDsid)`。数据库按 claimKey 去重,减少直接持久化可跨应用追踪的第三方设备指纹。确需审计原始 DSID 时应单独加密、限制访问并配置保留期限。 + +发券必须在一个事务中完成: + +1. `SELECT ... FOR UPDATE` challenge; +2. 检查用户、活动、地址、有效期和未消费状态; +3. 验证地址签名; +4. 验证设备 evidence / 读取已完成 relay session; +5. 推导 claimKey 并插入唯一约束; +6. 创建或占用 voucher; +7. 标记 challenge consumed; +8. 提交事务。 + +并发请求中只能有一个成功。网络重试返回同一个 `claimId/voucher`,不得再发一张。 + +## 6. 账户名称同步 + +名称同步不是发券认证的一部分,也不上传厂商完整数据库。 + +### Ledger Live + +Desktop 主进程读取 Ledger Live 的 `app.json`,只返回最小化候选: + +- `name` +- `address` + +当前实现只读取 `currency.id === "ethereum"` 且具有地址的账户,`xpub`、源账户 id +和完整账户对象不会跨越 Electron main/background IPC。匹配规则: + +1. 规范化 EVM 地址完全相同; +2. 同一地址出现多个不同名称时不建议同步; +3. UTXO/其他 EVM currency 暂不处理,避免跨链同地址误匹配。 + +当 Ledger Live 开启密码且账户字段为加密字符串时,返回 `encrypted_source`,不索取、保存或尝试破解 Ledger Live 密码。 + +### Trezor Suite + +Trezor Suite 的本地 `.mtdt` 标签为加密数据,需要设备执行 `CipherKeyValue` 派生 metadata master key;现代 Suite 也可能只使用 Dropbox、Google Drive 或 suite-sync。实现必须区分: + +- `available` +- `no_matches` +- `encrypted_source` +- `cloud_source_requires_authorization` +- `unsupported_source` + +没有取得 provider 授权或设备密钥时不得展示“已同步”。云端 provider 集成是独立后端/OAuth 工作,不应由本地文件读取逻辑伪装完成。 + +只有用户确认的候选名称调用 `setAccountName`。不得覆盖用户已在 OneKey 明确修改过的名称;冲突时逐项展示来源和目标名称。 + +## 7. App 触发条件 + +App 在开始创建钱包前记录 wallet IDs,创建结束后只在发现全新的 Trezor/Ledger wallet 时设置 post-add task: + +```text +vendor is trezor or ledger +AND createdWalletId not in existingWalletIds +AND postAddTask(walletId, campaignId) has never completed +``` + +弹窗顺序: + +1. 账户名称候选确认; +2. 活动说明与用户同意; +3. challenge、设备交互、地址签名、claim; +4. 成功或明确的可重试错误。 + +用户跳过、认证失败、厂商服务离线都不能阻止钱包创建或进入首页。普通重连和历史钱包恢复不触发活动弹窗。 + +## 8. 日志、隐私与可观测性 + +禁止日志记录: + +- nonce、relay token、完整 WSS URL; +- 原始证书链或签名; +- 原始 DSID / attestation public key; +- Ledger Live/Trezor Suite 完整账户数据; +- voucher code。 + +允许的结构化字段: + +- challenge/claim 的内部 UUID; +- vendor、model family、状态枚举; +- 经过批准的错误码; +- 分阶段耗时; +- 脱敏 campaignId。 + +必须有以下监控: + +- challenge 创建、过期、消费率; +- 每个厂商的 proof 验证失败率; +- Ledger 上游与 relay 完成率; +- 唯一约束冲突率; +- voucher 事务回滚率; +- 同一用户/IP/设备 claimKey 的异常频率。 + +## 9. 上线门槛 + +- Trezor:challenge 置换、重放、错用户、过期、debug root、错链、缺 Tropic、serial 不同、畸形/超大 DER 全部拒绝; +- Ledger:伪造 `{verified:true, deviceId}` 必须拒绝;relay session 过期、错用户、重放、APDU 篡改、non-genuine、缺 certificate、并发双提交全部拒绝; +- 两端:地址签名与 challenge 不匹配、活动重复领取、事务中途失败不发券; +- 名称同步:只在首次新建钱包触发、只改用户确认项、加密/云端来源不误报成功; +- 安全评审确认生产 root、吊销更新、Ledger 上游条款与凭据管理; +- 灰度阶段提供 kill switch,可分别关闭 Trezor、Ledger 或整个活动。 diff --git a/packages/hwk-adapter-core/src/index.ts b/packages/hwk-adapter-core/src/index.ts index bc38c8b7e..892327f48 100644 --- a/packages/hwk-adapter-core/src/index.ts +++ b/packages/hwk-adapter-core/src/index.ts @@ -115,6 +115,8 @@ export type { HardwareUiEvent, HardwareEventMap, DeviceEventListener, + DeviceAuthenticityParams, + DeviceAuthenticityResult, } from './types/wallet'; export { DEVICE_EVENT, DEVICE } from './events/device'; @@ -145,6 +147,7 @@ export type { ConnectorCallResult, ConnectorSerializedError, ConnectorErrorParams, + ConnectorConfig, IConnector, IHardwareBridge, } from './types/connector'; diff --git a/packages/hwk-adapter-core/src/types/connector.ts b/packages/hwk-adapter-core/src/types/connector.ts index d132fb367..887492bb7 100644 --- a/packages/hwk-adapter-core/src/types/connector.ts +++ b/packages/hwk-adapter-core/src/types/connector.ts @@ -175,6 +175,17 @@ export interface ConnectorSearchDevicesOptions { waitForAll?: boolean; } +export interface ConnectorConfig { + /** + * Optional OneKey-controlled WebSocket relay base used only for Ledger's + * genuine check. DMK appends its `/genuine` path and device query params. + * + * The value may contain a short-lived opaque relay token, so connectors must + * never log it. Passing `undefined` restores Ledger's official default. + */ + ledgerGenuineCheckWebSocketUrl?: string; +} + export interface IConnector { /** Physical connection type this connector uses. Fixed at construction. */ readonly connectionType: ConnectionType; @@ -199,6 +210,13 @@ export interface IConnector { reset(): void; + /** + * Reconfigure vendor-specific transport services before the next connect. + * Ledger uses this to rebuild DMK against a short-lived OneKey genuine-check + * relay. Vendors without runtime configuration may omit it. + */ + configure?(config: ConnectorConfig): Promise | void; + // Vendor-specific opt-in: warm-load persisted credentials before the first // session. Trezor uses this for THP `knownCredentials` (host-stored pairing // credentials replayed in `ThpHandshakeInitRequest` to skip the CodeEntry / @@ -237,6 +255,7 @@ export interface IHardwareBridge { cancel(params: { vendor: VendorType; sessionId: string }): Promise; uiResponse(params: { vendor: VendorType; response: UiResponseEvent }): void; reset(params: { vendor: VendorType }): void; + configure?(params: { vendor: VendorType; config: ConnectorConfig }): Promise | void; /** Register an event handler for connector events forwarded across the bridge. */ onEvent( @@ -312,6 +331,7 @@ export function createBridgedConnector( if (inner.size === 0) handlerMap.delete(event); }, reset: () => bridge.reset({ vendor }), + configure: bridge.configure ? config => bridge.configure?.({ vendor, config }) : undefined, setKnownCredentials: bridge.setKnownCredentials ? credentials => bridge.setKnownCredentials!({ vendor, credentials }) : undefined, @@ -710,6 +730,14 @@ export function createCombinedConnector(connectors: IConnector[]): IConnector { forwarders.clear(); }, + configure: connectors.some(c => c.configure) + ? async config => { + await Promise.all(connectors.map(child => Promise.resolve(child.configure?.(config)))); + deviceOwner.clear(); + sessionOwner.clear(); + } + : undefined, + setKnownCredentials: connectors.some(c => c.setKnownCredentials) ? async credentials => { // Seed the shared union from the host (warm-load) and fan out to all diff --git a/packages/hwk-adapter-core/src/types/wallet.ts b/packages/hwk-adapter-core/src/types/wallet.ts index 34e2e8b0b..a917a3b10 100644 --- a/packages/hwk-adapter-core/src/types/wallet.ts +++ b/packages/hwk-adapter-core/src/types/wallet.ts @@ -54,6 +54,57 @@ export type TrezorChangePinParams = { remove?: boolean; }; +export type DeviceAuthenticityParams = { + /** + * 32-byte server nonce encoded as hex. Trezor signs these exact bytes with + * its factory attestation key. Omit only for local diagnostics where the SDK + * may generate a random nonce itself. + */ + challenge?: string; + /** + * OneKey relay base returned by a short-lived backend challenge session. + * Ledger DMK connects to this URL instead of Ledger directly so the backend + * can witness the genuine-check transcript and issue a server-side receipt. + */ + ledgerGenuineCheckWebSocketUrl?: string; + /** + * Trezor development/simulator roots. Production reward flows must never + * enable this. + */ + dangerouslyAllowDebugKeys?: boolean; +}; + +export type DeviceAuthenticityResult = { + vendor: 'trezor' | 'ledger'; + verified: boolean; + deviceId?: string; + attestationPubKey?: string; + deviceCertPubKey?: string; + serialNumber?: string; + rootPubKey?: string; + caPubKey?: string; + usedDebugKey?: boolean; + error?: string; + note?: string; + /** + * Trezor-only server-verifiable envelope. The backend must verify this proof + * against the nonce stored for the challenge session; it must not trust the + * client's `verified` boolean. + */ + trezorProof?: { + challenge: string; + deviceModel: string; + proof: { + optiga_certificates: string[]; + optiga_signature: string; + tropic_certificates?: string[]; + tropic_signature?: string; + mcu_certificates?: string[]; + mcu_signature?: string; + }; + }; +}; + /** * Cross-chain / cross-vendor options passed alongside any chain method's * own params (the optional last argument). Holds operation-level switches @@ -425,6 +476,11 @@ export interface IHardwareWallet chain: ChainForFingerprint ): Promise>; + verifyDeviceAuthenticity?( + connectId: string, + params?: DeviceAuthenticityParams + ): Promise>; + // Events (notifications only: connect, disconnect, button, interaction) on( event: K, diff --git a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts index 3da26c905..26c3d43da 100644 --- a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts +++ b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts @@ -93,6 +93,8 @@ function createMockConnector(): IConnector & { }), reset: jest.fn(), + + configure: jest.fn().mockResolvedValue(undefined), }; return connector; @@ -129,6 +131,72 @@ describe('LedgerAdapter', () => { expect(adapter.vendor).toBe('ledger'); }); + it('routes genuine check through a short-lived relay and restores defaults', async () => { + const relayUrl = 'wss://attestation.onekey.test/session/opaque-token'; + connector.callImpl.mockResolvedValueOnce({ + isGenuine: true, + deviceId: 'ab'.repeat(32), + attestationPubKey: `04${'cd'.repeat(64)}`, + }); + + const result = await adapter.verifyDeviceAuthenticity('dev-1', { + ledgerGenuineCheckWebSocketUrl: relayUrl, + }); + + expect(connector.configure).toHaveBeenNthCalledWith(1, { + ledgerGenuineCheckWebSocketUrl: relayUrl, + }); + expect(connector.callImpl).toHaveBeenCalledWith('session-abc', 'getDeviceGenuineCheck', {}); + expect(connector.configure).toHaveBeenNthCalledWith(2, { + ledgerGenuineCheckWebSocketUrl: undefined, + }); + expect(result).toMatchObject({ + success: true, + payload: { + vendor: 'ledger', + verified: true, + deviceId: 'ab'.repeat(32), + }, + }); + }); + + it('does not expose attacker-controlled identity fields when Ledger is not genuine', async () => { + connector.callImpl.mockResolvedValueOnce({ + isGenuine: false, + deviceId: 'ab'.repeat(32), + attestationPubKey: `04${'cd'.repeat(64)}`, + }); + + const result = await adapter.verifyDeviceAuthenticity('dev-1'); + + expect(result).toEqual({ + success: true, + payload: { + vendor: 'ledger', + verified: false, + note: 'Ledger genuine-check returned NOT genuine.', + }, + }); + }); + + it('resets the connector if restoring the official Ledger endpoint fails', async () => { + const relayUrl = 'wss://attestation.onekey.test/session/opaque-token'; + connector.configure + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('offscreen bridge unavailable')); + connector.callImpl.mockResolvedValueOnce({ + isGenuine: true, + deviceId: 'ab'.repeat(32), + }); + + const result = await adapter.verifyDeviceAuthenticity('dev-1', { + ledgerGenuineCheckWebSocketUrl: relayUrl, + }); + + expect(result.success).toBe(true); + expect(connector.reset).toHaveBeenCalledTimes(1); + }); + describe('searchDevices', () => { it('should return devices from connector', async () => { const devices = await adapter.searchDevices(); diff --git a/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts b/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts index 5e72856dd..440296aa1 100644 --- a/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts +++ b/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts @@ -59,6 +59,69 @@ describe('LedgerConnectorBase error wrapping', () => { }); }); +describe('LedgerConnectorBase runtime genuine-check relay', () => { + it('rejects relay URLs that do not use secure WebSockets', async () => { + const connector = new LedgerConnectorBase(async () => ({})); + + await expect( + connector.configure({ + ledgerGenuineCheckWebSocketUrl: 'https://attestation.onekey.test/session', + }) + ).rejects.toThrow('must use wss'); + }); + + it('passes the short-lived relay base to the DMK builder', async () => { + const relayUrl = 'wss://attestation.onekey.test/session/opaque'; + const dmk = {}; + const builder = { + addTransport: jest.fn(), + addConfig: jest.fn(), + build: jest.fn(), + }; + builder.addTransport.mockReturnValue(builder); + builder.addConfig.mockReturnValue(builder); + builder.build.mockReturnValue(dmk); + const DeviceManagementKitBuilder = jest.fn(() => builder); + const connector = new LedgerConnectorBase(async () => () => ({})); + (connector as any)._importLedgerKit = jest.fn().mockResolvedValue({ + DeviceManagementKitBuilder, + }); + + await connector.configure({ + ledgerGenuineCheckWebSocketUrl: relayUrl, + }); + await (connector as any)._getOrCreateDmk(); + + expect(builder.addConfig).toHaveBeenCalledWith({ + webSocketUrl: relayUrl, + }); + expect(builder.build).toHaveBeenCalledTimes(1); + }); + + it('clears a configured relay URL on lifecycle reset', async () => { + const builder = { + addTransport: jest.fn(), + addConfig: jest.fn(), + build: jest.fn().mockReturnValue({}), + }; + builder.addTransport.mockReturnValue(builder); + builder.addConfig.mockReturnValue(builder); + const connector = new LedgerConnectorBase(async () => () => ({})); + (connector as any)._importLedgerKit = jest.fn().mockResolvedValue({ + DeviceManagementKitBuilder: jest.fn(() => builder), + }); + + await connector.configure({ + ledgerGenuineCheckWebSocketUrl: + 'wss://attestation.onekey.test/session/opaque', + }); + connector.reset(); + await (connector as any)._getOrCreateDmk(); + + expect(builder.addConfig).not.toHaveBeenCalled(); + }); +}); + describe('LedgerConnectorBase BLE discovery', () => { it('allows transport ids as BLE connectId even when they are not four-character names', async () => { const connector = new SearchConnector( diff --git a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts index beac8c273..d43fc78d4 100644 --- a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts +++ b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts @@ -51,6 +51,8 @@ import type { ConnectorCallResult, ConnectorDevice, ConnectorUiEvent, + DeviceAuthenticityParams, + DeviceAuthenticityResult, DeviceEventListener, DeviceInfo, DevicePermissionResponse, @@ -861,14 +863,33 @@ export class LedgerAdapter implements IHardwareWallet { * "Allow secure connection" confirmation the first time. */ async verifyDeviceAuthenticity( - connectId: string - ): Promise> { + connectId: string, + params: DeviceAuthenticityParams = {} + ): Promise> { + const relayUrl = params.ledgerGenuineCheckWebSocketUrl; try { + if (relayUrl) { + if (!this.connector.configure) { + return failure( + HardwareErrorCode.MethodNotSupported, + 'This Ledger connector does not support genuine-check relay configuration' + ); + } + await this.connector.configure({ ledgerGenuineCheckWebSocketUrl: relayUrl }); + this.resetState(); + } const result = (await this.connectorCall(connectId, 'getDeviceGenuineCheck', {})) as { isGenuine: boolean; deviceId?: string; attestationPubKey?: string; }; + if (!result.isGenuine) { + return success({ + vendor: 'ledger' as const, + verified: false, + note: 'Ledger genuine-check returned NOT genuine.', + }); + } if (!result.deviceId) { // The genuine verdict came back, but DMK did not surface a deviceId // (server did not drive GET CERTIFICATE, or the certificate failed to @@ -879,15 +900,26 @@ export class LedgerAdapter implements IHardwareWallet { ); } return success({ + vendor: 'ledger' as const, verified: result.isGenuine, deviceId: result.deviceId, attestationPubKey: result.attestationPubKey, - note: result.isGenuine - ? 'Verified by Ledger genuine-check backend; deviceId = sha3_256(attestation pubkey).' - : 'Ledger genuine-check returned NOT genuine — do not trust this device.', + note: 'Verified by Ledger genuine-check backend; deviceId = sha3_256(attestation pubkey).', }); } catch (err) { return this.errorToFailure(err); + } finally { + if (relayUrl) { + try { + await this.connector.configure?.({ ledgerGenuineCheckWebSocketUrl: undefined }); + this.resetState(); + } catch { + // Fail safe: reset() also clears the one-shot relay URL in connectors + // that implement runtime relay configuration. + this.connector.reset(); + this.resetState(); + } + } } } diff --git a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts index 3e1318a66..dd6976ade 100644 --- a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts +++ b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts @@ -4,6 +4,7 @@ import { isKnownNonTargetHardwareVendor, serializeConnectorError, } from '@onekeyfe/hwk-adapter-core'; +import * as sha3 from '@noble/hashes/sha3'; import { LedgerDeviceManager } from '../device/LedgerDeviceManager'; import { SignerManager } from '../signer/SignerManager'; @@ -35,35 +36,9 @@ import { tronSignTransaction, } from './chains'; import { DeviceAppsManager } from '../device-apps/DeviceAppsManager'; -import { sha3_256 } from '@noble/hashes/sha3'; - import { collapseSignerInteraction } from './chains/utils'; import { deviceActionToPromise } from '../signer/deviceActionToPromise'; -// GET CERTIFICATE (E0 52) response = length-value fields: a header field followed -// by the attestation public key field. Mirrors DMK's `extractPublicKey`: skip the -// first LV field, return the second (65-byte uncompressed EC point). -function extractCertPubKey(data: Uint8Array): string | null { - try { - const buf = Buffer.from(data); - let offset = 0; - if (offset >= buf.length) return null; - const headerLen = buf[offset]; - offset += 1 + headerLen; - if (offset >= buf.length) return null; - const keyLen = buf[offset]; - offset += 1; - if (keyLen === 0 || offset + keyLen > buf.length) return null; - return buf.subarray(offset, offset + keyLen).toString('hex'); - } catch { - return null; - } -} - -function sha3HexOf(pubKeyHex: string): string { - return Buffer.from(sha3_256(Uint8Array.from(Buffer.from(pubKeyHex, 'hex')))).toString('hex'); -} - import type { DeviceApps, InstallAppCallParams, @@ -76,6 +51,7 @@ import type { DeviceManagementKit } from '@ledgerhq/device-management-kit'; import type { ConnectionType, ConnectorCallResult, + ConnectorConfig, ConnectorDevice, ConnectorEventMap, ConnectorEventType, @@ -103,6 +79,30 @@ import type { TronSignTransactionCallParams, } from './chains'; +// GET CERTIFICATE (E0 52) response = length-value fields: a header field followed +// by the attestation public key field. Mirrors DMK's `extractPublicKey`: skip the +// first LV field, return the second (65-byte uncompressed EC point). +function extractCertPubKey(data: Uint8Array): string | null { + try { + const buf = Buffer.from(data); + let offset = 0; + if (offset >= buf.length) return null; + const headerLen = buf[offset]; + offset += 1 + headerLen; + if (offset >= buf.length) return null; + const keyLen = buf[offset]; + offset += 1; + if (keyLen === 0 || offset + keyLen > buf.length) return null; + return buf.subarray(offset, offset + keyLen).toString('hex'); + } catch { + return null; + } +} + +function sha3HexOf(pubKeyHex: string): string { + return Buffer.from(sha3.sha3_256(Uint8Array.from(Buffer.from(pubKeyHex, 'hex')))).toString('hex'); +} + // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- @@ -233,6 +233,8 @@ export class LedgerConnectorBase implements IConnector { private readonly _createTransport: TransportFactory; + private _ledgerGenuineCheckWebSocketUrl: string | undefined; + public readonly connectionType: ConnectionType; // --------------------------------------------------------------------------- @@ -891,10 +893,7 @@ export class LedgerConnectorBase implements IConnector { dmk as unknown as { // Loose return: deviceActionToPromise narrows the observable shape. // eslint-disable-next-line @typescript-eslint/no-explicit-any - executeDeviceAction(args: { - sessionId: string; - deviceAction: unknown; - }): any; + executeDeviceAction(args: { sessionId: string; deviceAction: unknown }): any; } ).executeDeviceAction({ sessionId, @@ -913,13 +912,13 @@ export class LedgerConnectorBase implements IConnector { // "Allow secure connection" tap (the idle watchdog does not pause // for it, only for unlock-device). 5 * 60_000, - (cancel) => ctx.registerCanceller(sessionId, cancel), - (intermediateValue) => { + cancel => ctx.registerCanceller(sessionId, cancel), + intermediateValue => { const iv = intermediateValue as { deviceId?: Uint8Array } | undefined; if (iv?.deviceId instanceof Uint8Array && !deviceId) { deviceId = Buffer.from(iv.deviceId).toString('hex'); } - }, + } ); // Best-effort: recover the raw attestation public key that DMK hashed @@ -927,7 +926,7 @@ export class LedgerConnectorBase implements IConnector { // the exact key behind this id (not some other cert seen on the wire). const attestationPubKey = deviceId && this._capturedGenuineCertPubKeys.length > 0 - ? this._capturedGenuineCertPubKeys.find((pk) => sha3HexOf(pk) === deviceId) + ? this._capturedGenuineCertPubKeys.find(pk => sha3HexOf(pk) === deviceId) : undefined; this._capturedGenuineCertPubKeys = []; @@ -987,9 +986,35 @@ export class LedgerConnectorBase implements IConnector { // --------------------------------------------------------------------------- reset(): void { + // A relay URL is a single-use capability. Any lifecycle reset must restore + // the official Ledger default so it cannot bleed into a later operation. + this._ledgerGenuineCheckWebSocketUrl = undefined; this._resetAll(); } + async configure(config: ConnectorConfig): Promise { + const nextUrl = config.ledgerGenuineCheckWebSocketUrl; + if (nextUrl) { + let parsed: URL; + try { + parsed = new URL(nextUrl); + } catch { + throw new Error('Ledger genuine-check relay URL is invalid'); + } + if (parsed.protocol !== 'wss:') { + throw new Error('Ledger genuine-check relay URL must use wss'); + } + if (this._providedDmk) { + throw new Error('Cannot change Ledger genuine-check relay on a pre-built DMK'); + } + } + if (nextUrl === this._ledgerGenuineCheckWebSocketUrl) { + return; + } + this._resetAll(); + this._ledgerGenuineCheckWebSocketUrl = nextUrl; + } + // --------------------------------------------------------------------------- // Private -- DMK / Manager lifecycle // --------------------------------------------------------------------------- @@ -1009,7 +1034,6 @@ export class LedgerConnectorBase implements IConnector { // tap is best-effort and can NEVER disturb the real APDU flow (it always // returns the original result, all capture logic is in try/catch). private _tapTransportForCert(factory: unknown): unknown { - const self = this; return (args: unknown) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const transport = (factory as (a: unknown) => any)(args); @@ -1021,7 +1045,7 @@ export class LedgerConnectorBase implements IConnector { try { if (either && typeof either.map === 'function') { // purify-ts Either: map only touches the Right (connected device). - return either.map((device: unknown) => self._tapConnectedDevice(device)); + return either.map((device: unknown) => this._tapConnectedDevice(device)); } } catch { /* fall through, return untouched */ @@ -1035,7 +1059,6 @@ export class LedgerConnectorBase implements IConnector { // eslint-disable-next-line @typescript-eslint/no-explicit-any private _tapConnectedDevice(device: any): unknown { - const self = this; const originalSendApdu = device?.sendApdu; if (typeof originalSendApdu !== 'function') return device; const wrapped = async (apdu: Uint8Array, ...rest: unknown[]) => { @@ -1051,7 +1074,7 @@ export class LedgerConnectorBase implements IConnector { const resp = result.extract() as { statusCode: Uint8Array; data: Uint8Array }; if (resp && Buffer.from(resp.statusCode).toString('hex') === '9000') { const pubKey = extractCertPubKey(resp.data); - if (pubKey) self._capturedGenuineCertPubKeys.push(pubKey); + if (pubKey) this._capturedGenuineCertPubKeys.push(pubKey); } } } catch { @@ -1088,9 +1111,11 @@ export class LedgerConnectorBase implements IConnector { debugLog('[DMK] _getOrCreateDmk: transportFactory type:', typeof transportFactory); - const dmk: DeviceManagementKit = new DeviceManagementKitBuilder() - .addTransport(transportFactory) - .build(); + const builder = new DeviceManagementKitBuilder().addTransport(transportFactory); + if (this._ledgerGenuineCheckWebSocketUrl) { + builder.addConfig({ webSocketUrl: this._ledgerGenuineCheckWebSocketUrl }); + } + const dmk: DeviceManagementKit = builder.build(); this._dmk = dmk; debugLog('[DMK] _getOrCreateDmk: DMK created'); diff --git a/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts b/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts index 26490603d..90b2ef50c 100644 --- a/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts +++ b/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts @@ -55,6 +55,72 @@ describe('TrezorAdapter', () => { }); }); + it('uses the exact server challenge and returns the raw attestation proof', async () => { + const connector = createConnector(); + const challenge = 'ab'.repeat(32); + const proof = { + optiga_certificates: ['00'], + optiga_signature: '11', + }; + (connector.call as CallMock) + .mockResolvedValueOnce({ internal_model: 'T3W1' }) + .mockResolvedValueOnce(proof); + const adapter = new TrezorAdapter(connector); + + const result = await adapter.verifyDeviceAuthenticity('safe-7', { challenge }); + + expect(connector.call).toHaveBeenNthCalledWith(2, 'safe-7-session', 'authenticateDevice', { + challenge, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.payload).toMatchObject({ + vendor: 'trezor', + trezorProof: { + challenge, + deviceModel: 'T3W1', + proof, + }, + }); + } + }); + + it('rejects a malformed server challenge before connecting to the device', async () => { + const connector = createConnector(); + const adapter = new TrezorAdapter(connector); + + const result = await adapter.verifyDeviceAuthenticity('safe-7', { challenge: 'abcd' }); + + expect(result).toEqual({ + success: false, + payload: { + code: HardwareErrorCode.InvalidParams, + error: 'Device authenticity challenge must be exactly 32 bytes encoded as hex', + }, + }); + expect(connector.connect).not.toHaveBeenCalled(); + expect(connector.call).not.toHaveBeenCalled(); + }); + + it('never allows debug roots in a server-challenge reward flow', async () => { + const connector = createConnector(); + const adapter = new TrezorAdapter(connector); + + const result = await adapter.verifyDeviceAuthenticity('safe-7', { + challenge: 'ab'.repeat(32), + dangerouslyAllowDebugKeys: true, + }); + + expect(result).toEqual({ + success: false, + payload: { + code: HardwareErrorCode.InvalidParams, + error: 'Debug attestation roots cannot be used with a server challenge', + }, + }); + expect(connector.connect).not.toHaveBeenCalled(); + }); + it('maps WebUSB transfer errors during connect to TransportError', async () => { const connector = createConnector(); (connector.connect as ConnectMock).mockRejectedValueOnce( diff --git a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts index 84703ceb7..f644d7b1b 100644 --- a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts +++ b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts @@ -11,15 +11,12 @@ import { runAllNetworkGetAddress, success, } from '@onekeyfe/hwk-adapter-core'; - import { randomBytes } from '@noble/hashes/utils'; import { authenticateDeviceFromProof } from '../deviceAuthenticity'; - import { debugLog } from '../utils/debugLog'; -import type { AuthenticateDeviceResult, AuthenticityProof } from '../deviceAuthenticity'; - +import type { AuthenticityProof } from '../deviceAuthenticity'; import type { AllNetworkAddressParams, AllNetworkAddressResponse, @@ -40,6 +37,8 @@ import type { ConnectorCallResult, ConnectorDevice, ConnectorUiEvent, + DeviceAuthenticityParams, + DeviceAuthenticityResult, DeviceEventListener, DeviceInfo, EvmAddress, @@ -678,8 +677,21 @@ export class TrezorAdapter implements IHardwareWallet { */ async verifyDeviceAuthenticity( connectId: string, - params: { dangerouslyAllowDebugKeys?: boolean } = {} - ): Promise> { + params: DeviceAuthenticityParams = {} + ): Promise> { + if (params.challenge && params.dangerouslyAllowDebugKeys) { + return failure( + HardwareErrorCode.InvalidParams, + 'Debug attestation roots cannot be used with a server challenge' + ); + } + if (params.challenge && !/^[0-9a-fA-F]{64}$/.test(params.challenge)) { + return failure( + HardwareErrorCode.InvalidParams, + 'Device authenticity challenge must be exactly 32 bytes encoded as hex' + ); + } + const featuresRes = await this.getFeatures(connectId); if (!featuresRes.success) return featuresRes; @@ -688,22 +700,33 @@ export class TrezorAdapter implements IHardwareWallet { return failure(HardwareErrorCode.UnknownError, 'Device internal_model unavailable'); } - const challenge = Buffer.from(randomBytes(32)); + const challengeHex = + params.challenge?.toLowerCase() ?? Buffer.from(randomBytes(32)).toString('hex'); + const challenge = Buffer.from(challengeHex, 'hex'); const proofRes = await this._callDeviceManagerMethod>( 'authenticateDevice', connectId, - { challenge: challenge.toString('hex') } + { challenge: challengeHex } ); if (!proofRes.success) return proofRes; - return success( - authenticateDeviceFromProof({ - proof: proofRes.payload as unknown as AuthenticityProof, - challenge, + const proof = proofRes.payload as unknown as AuthenticityProof; + const verification = authenticateDeviceFromProof({ + proof, + challenge, + deviceModel: internalModel, + allowDebugKeys: params.dangerouslyAllowDebugKeys, + }); + + return success({ + ...verification, + vendor: 'trezor' as const, + trezorProof: { + challenge: challengeHex, deviceModel: internalModel, - allowDebugKeys: params.dangerouslyAllowDebugKeys, - }) - ); + proof, + }, + }); } deviceSettings( diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts index db2f33d5c..11fb5b522 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts @@ -1,4 +1,4 @@ -import { authenticateDeviceFromProof } from '..'; +import { authenticateDeviceFromProof, getRequiredDeviceAuthenticityLayers } from '..'; import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from '../verifyAuthenticityProof'; import type { DeviceAuthenticityConfig } from '../types'; @@ -24,8 +24,7 @@ const DEVICE_CERT_TROPIC = '308201993082014ba003020102020868b1982dacb2b041300506032b6570304f310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3120301e06035504030c175472657a6f72204d616e75666163747572696e672043413020170d3235303832393132353934375a180f32303535303832323132353934375a3051311b301906035504030c1254335731205472657a6f72205361666520373121301f06035504051318343732303930323235323232323232323232323232323232310f300d060355042e130654726f706963302a300506032b6570032100c92482676994f4e2dec7eb2bb6a9ce9a8a38d4536c5e16b26646015cd7034593a341303f300e0603551d0f0101ff040403020080300c0603551d130101ff04023000301f0603551d23041830168014cf35aa12a033c044ebc6c3c0c3aefea5ae5e7db4300506032b65700341003b0aa3ccdddfd2a473e3f36059d0da2e54428c8dca5050a5fabf845ca71365d8045cc4d15c4e6e37565cbd66cdb4ba8d7ca113941f596a9bbfc25c069cf91903'; const SIGNATURE_TROPIC = '9d6a7cfc1d9957a7eaa09f58ab385a4722dc621c6a58f0e281305f36c1447206ea6642f4e4ff36207bd57c3719101855e9c9a00a5db60cc84e20181d69f4fa00'; -const T3W1_ROOT_PUB_KEY_TROPIC = - 'cd318dc8405ae4f4144e3284dcb7b0cb0f0c2195c2ca14a0f6fccd9104e32a4b'; +const T3W1_ROOT_PUB_KEY_TROPIC = 'cd318dc8405ae4f4144e3284dcb7b0cb0f0c2195c2ca14a0f6fccd9104e32a4b'; const SERIAL_TROPIC = '343732303930323235323232323232323232323232323232'; const CONFIG: DeviceAuthenticityConfig = { @@ -99,6 +98,11 @@ describe('verifyAuthenticityProof', () => { }); describe('authenticateDeviceFromProof', () => { + it('keeps T3W1 Tropic requirements independent from root-key configuration', () => { + expect(getRequiredDeviceAuthenticityLayers('T3W1')).toEqual(['optiga', 'tropic']); + expect(getRequiredDeviceAuthenticityLayers('T2B1')).toEqual(['optiga']); + }); + it('derives a stable per-device id (sha256 of the device pubkey when no serial)', () => { const result = authenticateDeviceFromProof({ proof: { @@ -171,7 +175,10 @@ describe('authenticateDeviceFromProof', () => { it('requires the Tropic layer on T3W1 (optiga-only proof is rejected)', () => { const t3w1Config: DeviceAuthenticityConfig = { version: 1, - T3W1: { rootPubKeysOptiga: [T2B1_ROOT_PUB_KEY_OPTIGA], rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC] }, + T3W1: { + rootPubKeysOptiga: [T2B1_ROOT_PUB_KEY_OPTIGA], + rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC], + }, }; const result = authenticateDeviceFromProof({ // reuse the optiga vector but omit tropic_* → tropic required-but-missing diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts index 57f0b4d97..7d849334f 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts @@ -3,7 +3,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { deviceAuthenticityConfig } from './config'; import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from './verifyAuthenticityProof'; -import type { DeviceAuthenticityConfig, VerifyAuthenticityProofResult } from './types'; +import type { DeviceAuthenticityConfig } from './types'; export { deviceAuthenticityConfig } from './config'; export { prepareDeviceAuthenticityData, verifyAuthenticityProof } from './verifyAuthenticityProof'; @@ -21,12 +21,13 @@ export type AuthenticityProof = { export type AuthenticateDeviceResult = { /** - * True only when EVERY required attestation layer for this model verified up - * to a trusted Trezor root CA AND the device signed our challenge: + * True only when every layer implemented by this verifier and required by + * the explicit model policy verified up to a trusted Trezor root CA AND the + * device signed our challenge: * - Optiga (all secure-element models), and - * - Tropic + matching serial number (T3W1 and above). - * ML-DSA / MCU is NOT checked (needs @noble/post-quantum). Only a `true` - * value may be trusted as a genuine, uniquely-identified physical device. + * - Tropic + matching serial number (T3W1). + * ML-DSA / MCU is NOT checked (needs @noble/post-quantum). The reward backend + * must independently verify raw proof under its own versioned policy. */ verified: boolean; /** @@ -62,29 +63,23 @@ const modelConfigOf = (config: DeviceAuthenticityConfig, deviceModel: string) => return modelConfig && typeof modelConfig !== 'number' ? modelConfig : undefined; }; -const isTropicExpected = ( - config: DeviceAuthenticityConfig, - deviceModel: string, - allowDebugKeys?: boolean, -): boolean => { - const m = modelConfigOf(config, deviceModel); - if (!m) return false; - return ( - (m.rootPubKeysTropic?.length ?? 0) > 0 || - (!!allowDebugKeys && (m.debug?.rootPubKeysTropic?.length ?? 0) > 0) - ); -}; +export const getRequiredDeviceAuthenticityLayers = ( + deviceModel: string +): readonly ('optiga' | 'tropic')[] => (deviceModel === 'T3W1' ? ['optiga', 'tropic'] : ['optiga']); + +const isTropicExpected = (deviceModel: string): boolean => + getRequiredDeviceAuthenticityLayers(deviceModel).includes('tropic'); const matchedDebugKey = ( config: DeviceAuthenticityConfig, deviceModel: string, - rootPubKey?: string, + rootPubKey?: string ): boolean => { if (!rootPubKey) return false; const m = modelConfigOf(config, deviceModel); if (!m?.debug) return false; return [...(m.debug.rootPubKeysOptiga ?? []), ...(m.debug.rootPubKeysTropic ?? [])].includes( - rootPubKey, + rootPubKey ); }; @@ -112,14 +107,14 @@ export const authenticateDeviceFromProof = ({ const common = { signedData, deviceModel, config, allowDebugKeys, caPubKeyBlacklist }; // 1) Optiga is required on every attestation-capable model. - const { optiga_certificates, optiga_signature } = proof; - if (!optiga_signature || !optiga_certificates?.length) { + const { optiga_certificates: optigaCertificates, optiga_signature: optigaSignature } = proof; + if (!optigaSignature || !optigaCertificates?.length) { return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; } const optiga = verifyAuthenticityProof({ ...common, - certificates: optiga_certificates, - signature: optiga_signature, + certificates: optigaCertificates, + signature: optigaSignature, }); if (!optiga.valid) { return { @@ -131,19 +126,19 @@ export const authenticateDeviceFromProof = ({ }; } - // 2) On T3W1+ the Tropic layer is also required, and its serial number must + // 2) On T3W1 the Tropic layer is also required, and its serial number must // match Optiga's. This defeats a transplanted-Optiga forgery: an attacker // moving a genuine Optiga onto a fake board cannot also satisfy Tropic. let usedDebugKey = matchedDebugKey(config, deviceModel, optiga.rootPubKey); - if (isTropicExpected(config, deviceModel, allowDebugKeys)) { - const { tropic_certificates, tropic_signature } = proof; - if (!tropic_signature || !tropic_certificates?.length) { + if (isTropicExpected(deviceModel)) { + const { tropic_certificates: tropicCertificates, tropic_signature: tropicSignature } = proof; + if (!tropicSignature || !tropicCertificates?.length) { return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; } const tropic = verifyAuthenticityProof({ ...common, - certificates: tropic_certificates, - signature: tropic_signature, + certificates: tropicCertificates, + signature: tropicSignature, }); if (!tropic.valid) { return { verified: false, error: tropic.error }; @@ -152,14 +147,13 @@ export const authenticateDeviceFromProof = ({ if (!serialsPresent || optiga.serialNumber !== tropic.serialNumber) { return { verified: false, error: 'SERIAL_NUMBER_MISMATCH' }; } - usedDebugKey = - usedDebugKey || matchedDebugKey(config, deviceModel, tropic.rootPubKey); + usedDebugKey = usedDebugKey || matchedDebugKey(config, deviceModel, tropic.rootPubKey); } const deviceId = optiga.serialNumber ?? Buffer.from(sha256(Uint8Array.from(Buffer.from(optiga.deviceCertPubKey, 'hex')))).toString( - 'hex', + 'hex' ); return { @@ -180,6 +174,3 @@ export const authenticateDeviceFromProof = ({ }; } }; - -// re-export for callers that want the low-level result shape -export type { VerifyAuthenticityProofResult }; From d473bb46c786556950e4d6309a8ebcf024ffa713 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Sun, 26 Jul 2026 09:58:32 +0800 Subject: [PATCH 03/12] fix: serialize Ledger attestation relay sessions --- .../src/__tests__/LedgerAdapter.test.ts | 55 +++++++++++++++++++ .../src/adapter/LedgerAdapter.ts | 22 ++++++++ 2 files changed, 77 insertions(+) diff --git a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts index 26c3d43da..00417519d 100644 --- a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts +++ b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts @@ -197,6 +197,61 @@ describe('LedgerAdapter', () => { expect(connector.reset).toHaveBeenCalledTimes(1); }); + it('serializes complete one-shot Ledger relay lifecycles', async () => { + const relayOne = 'wss://attestation.onekey.test/session/relay-one'; + const relayTwo = 'wss://attestation.onekey.test/session/relay-two'; + let resolveFirstCheck: (value: { isGenuine: boolean; deviceId: string }) => void = () => + undefined; + connector.callImpl + .mockReturnValueOnce( + new Promise(resolve => { + resolveFirstCheck = resolve; + }) + ) + .mockResolvedValueOnce({ + isGenuine: true, + deviceId: 'bb'.repeat(32), + }); + + const first = adapter.verifyDeviceAuthenticity('dev-1', { + ledgerGenuineCheckWebSocketUrl: relayOne, + }); + await waitForCondition( + () => + connector.callImpl.mock.calls.filter(call => call[1] === 'getDeviceGenuineCheck').length === + 1 + ); + const second = adapter.verifyDeviceAuthenticity('dev-1', { + ledgerGenuineCheckWebSocketUrl: relayTwo, + }); + await Promise.resolve(); + + expect(connector.configure).toHaveBeenCalledTimes(1); + expect(connector.configure).toHaveBeenLastCalledWith({ + ledgerGenuineCheckWebSocketUrl: relayOne, + }); + + resolveFirstCheck({ + isGenuine: true, + deviceId: 'aa'.repeat(32), + }); + await expect(first).resolves.toMatchObject({ + success: true, + payload: { deviceId: 'aa'.repeat(32) }, + }); + await expect(second).resolves.toMatchObject({ + success: true, + payload: { deviceId: 'bb'.repeat(32) }, + }); + + expect(connector.configure.mock.calls).toEqual([ + [{ ledgerGenuineCheckWebSocketUrl: relayOne }], + [{ ledgerGenuineCheckWebSocketUrl: undefined }], + [{ ledgerGenuineCheckWebSocketUrl: relayTwo }], + [{ ledgerGenuineCheckWebSocketUrl: undefined }], + ]); + }); + describe('searchDevices', () => { it('should return devices from connector', async () => { const devices = await adapter.searchDevices(); diff --git a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts index d43fc78d4..faa8676b1 100644 --- a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts +++ b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts @@ -181,6 +181,11 @@ export class LedgerAdapter implements IHardwareWallet { // BLE-only parallelism isn't worth the coordination cost. private readonly _jobQueue: DeviceJobQueue; + // Runtime relay configuration mutates connector-wide DMK state. Serialize + // the complete configure → genuine check → clear lifecycle so concurrent + // callers cannot reset or overwrite each other's one-shot relay. + private _deviceAuthenticityQueueTail: Promise = Promise.resolve(); + // Shared across concurrent callers — only `cancel()` aborts. private _doConnectAbortController: AbortController | null = null; @@ -865,6 +870,23 @@ export class LedgerAdapter implements IHardwareWallet { async verifyDeviceAuthenticity( connectId: string, params: DeviceAuthenticityParams = {} + ): Promise> { + const waitForPrevious = this._deviceAuthenticityQueueTail; + let releaseQueue: () => void = () => undefined; + this._deviceAuthenticityQueueTail = new Promise(resolve => { + releaseQueue = resolve; + }); + await waitForPrevious; + try { + return await this._verifyDeviceAuthenticityExclusive(connectId, params); + } finally { + releaseQueue(); + } + } + + private async _verifyDeviceAuthenticityExclusive( + connectId: string, + params: DeviceAuthenticityParams ): Promise> { const relayUrl = params.ledgerGenuineCheckWebSocketUrl; try { From 194bd3863fb711bdbc3b05cb27bf2388290ff80d Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Mon, 27 Jul 2026 01:14:16 +0800 Subject: [PATCH 04/12] fix: retry Trezor after stale THP frame --- .../src/__tests__/TrezorAdapter.test.ts | 33 ++++++++++++++++++ .../src/adapter/TrezorAdapter.ts | 34 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts b/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts index 90b2ef50c..84f6435f9 100644 --- a/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts +++ b/packages/hwk-trezor-adapter/src/__tests__/TrezorAdapter.test.ts @@ -137,6 +137,39 @@ describe('TrezorAdapter', () => { } }); + it('retries once when an interrupted THP handshake leaves one malformed frame', async () => { + const connector = createConnector(); + (connector.connect as ConnectMock).mockRejectedValueOnce( + Object.assign(new Error('Malformed protocol format'), { + name: 'TrezorProtocolError', + code: 'Malformed protocol format', + }) + ); + const adapter = new TrezorAdapter(connector); + + await expect(adapter.connectDevice('safe-7')).resolves.toEqual({ + success: true, + payload: 'safe-7', + }); + expect(connector.connect).toHaveBeenCalledTimes(2); + }); + + it('does not retry unrelated Trezor protocol errors', async () => { + const connector = createConnector(); + (connector.connect as ConnectMock).mockRejectedValueOnce( + Object.assign(new Error('Unexpected protocol version'), { + name: 'TrezorProtocolError', + code: 'Unexpected protocol version', + }) + ); + const adapter = new TrezorAdapter(connector); + + const result = await adapter.connectDevice('safe-7'); + + expect(result.success).toBe(false); + expect(connector.connect).toHaveBeenCalledTimes(1); + }); + it('emits request and failed response logs with the device error details', async () => { const connector = createConnector(); (connector.call as CallMock) diff --git a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts index f644d7b1b..47df9e18a 100644 --- a/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts +++ b/packages/hwk-trezor-adapter/src/adapter/TrezorAdapter.ts @@ -420,6 +420,19 @@ export class TrezorAdapter implements IHardwareWallet { ); } + private static _isRecoverableProtocolResidueError(error: unknown): boolean { + const protocolError = error as { + name?: unknown; + code?: unknown; + message?: unknown; + }; + return ( + protocolError.name === 'TrezorProtocolError' && + protocolError.code === 'Malformed protocol format' && + protocolError.message === 'Malformed protocol format' + ); + } + /** * Race a promise against an abort signal. Copied from LedgerAdapter: * IConnector.call() can't actually be cancelled at the protocol level, but @@ -451,8 +464,8 @@ export class TrezorAdapter implements IHardwareWallet { } private async _connectSession(connectId: string, signal?: AbortSignal): Promise { - const connectPromise = this._connector.connect(connectId); - const session = await (async () => { + const connectOnce = async () => { + const connectPromise = this._connector.connect(connectId); if (!signal) return connectPromise; try { return await TrezorAdapter._abortable(signal, connectPromise); @@ -465,7 +478,22 @@ export class TrezorAdapter implements IHardwareWallet { ); throw error; } - })(); + }; + let session; + try { + session = await connectOnce(); + } catch (error) { + if (signal?.aborted || !TrezorAdapter._isRecoverableProtocolResidueError(error)) { + throw error; + } + // An interrupted THP handshake may leave exactly one v2 frame queued on + // the shared USB pipe. The connector intentionally drains it as a loud + // v1 malformed-frame failure; retry once now that the residue is gone. + debugLog('[TrezorAdapter] retrying connect after draining a stale THP frame', { + connectId, + }); + session = await connectOnce(); + } // disconnectDevice() ran mid-connect: tear down the now-unwanted session // instead of caching it (else it leaks a limited THP slot). if (this._disconnectRequested.delete(connectId)) { From 95480392129de7531a9681363d69a00345e01c44 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Mon, 27 Jul 2026 16:20:09 +0800 Subject: [PATCH 05/12] feat: expose Trezor attestation verifier --- packages/hwk-trezor-adapter/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/hwk-trezor-adapter/src/index.ts b/packages/hwk-trezor-adapter/src/index.ts index 9bff4fe0f..46385e9f2 100644 --- a/packages/hwk-trezor-adapter/src/index.ts +++ b/packages/hwk-trezor-adapter/src/index.ts @@ -1,4 +1,11 @@ export { TrezorAdapter } from './adapter/TrezorAdapter'; +// Pure verifier for backend/mock-backend attestation validation. It performs +// no device I/O and never trusts the adapter's client-side `verified` verdict. +export { + authenticateDeviceFromProof, + getRequiredDeviceAuthenticityLayers, +} from './deviceAuthenticity'; +export type { AuthenticateDeviceResult, AuthenticityProof } from './deviceAuthenticity'; // Opt-in definition-fetch helpers. NOT called by any signing method — callers // fetch (through their own `baseUrl` proxy if desired) and inject the bytes into // the sign params (`ethereumDefinitions` / `additionalInfo.encodedToken`). From 2fa2f2f2ecc7d6c62d2741a053f20eb8d3386439 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Mon, 27 Jul 2026 23:02:04 +0800 Subject: [PATCH 06/12] feat: complete Trezor Safe 7 attestation proof --- HANDOFF-device-attestation.md | 83 ++- docs/device-attestation-voucher-backend.md | 60 +- packages/hwk-trezor-adapter/jest.config.js | 1 + packages/hwk-trezor-adapter/package.json | 1 + .../__tests__/mcuFixture.ts | 8 + .../__tests__/verifyAuthenticityProof.test.ts | 72 +- .../src/deviceAuthenticity/config.ts | 5 +- .../src/deviceAuthenticity/index.ts | 54 +- .../deviceAuthenticity/trezorRootKeysMLDSA.ts | 6 + .../verifyAuthenticityProof.ts | 95 ++- .../deviceAuthenticity/verifySignatures.ts | 16 +- packages/hwk-trezor-adapter/tsup.config.ts | 4 + .../src/__tests__/TrezorConnectorBase.test.ts | 137 ++++ packages/hwk-trezor-connector/src/index.ts | 160 ++++- .../src/definitions/index.ts | 13 +- .../src/definitions/messages-management.ts | 42 ++ .../src/definitions/messages-management_pb.js | 667 +++++++++++++++--- .../src/definitions/messages_pb.js | 34 +- .../hwk-trezor-protobuf/src/manager.test.ts | 45 +- packages/hwk-trezor-protobuf/src/manager.ts | 7 +- yarn.lock | 26 + 21 files changed, 1347 insertions(+), 189 deletions(-) create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts create mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index e83984971..afc68eda5 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -1,21 +1,28 @@ # 交接文档:第三方硬件设备唯一 ID + 账户名迁移 -> 最后更新:2026-07-26 -> 涉及两条工作线:**① 设备唯一 ID(设备认证 / attestation)—— 已实现 + 真机验证通过**;**② 从 Trezor Suite / Ledger Live 迁移账户名 —— 仅调研,未实现**。 +> 最后更新:2026-07-27 +> 本文保留了早期调查过程;以本节状态和 +> `docs/device-attestation-voucher-backend.md` 的生产边界为准。 + +> **重要纠正**:早期 T3W1 真机结果只覆盖 Optiga + Tropic,不能代表当前要求的 +> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。Ledger 的本地 +> DMK Genuine Check 是真实厂商在线验真,但它不是可上传给 OneKey 后端独立信任的 +> 离线证明。 --- ## 0. 状态速览 -| 项 | 状态 | 真机验证 | -|---|---|---| -| Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已实现 | ✅ T3W1 `Verified ✅` | -| Ledger 设备唯一 ID(DMK genuine-check 走后端) | ✅ 已实现 | ✅ Nano X `Verified ✅` | -| Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | -| Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | -| app-monorepo 调试页(Developer→Gallery→DeviceAuthenticity) | ✅ 已加 | ✅ 已用它测通两端 | -| 账户名迁移 · Ledger | 🔬 调研完成:**可行(纯本地明文)** | — | -| 账户名迁移 · Trezor | 🔬 调研完成:**难(需接设备 + 云端文件)** | — | +| 项 | 状态 | 真机验证 | +| ----------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | +| Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已补齐流式 proof + Optiga/Tropic/MCU 三层验证 | ⏳ 当前版本待 Safe 7 重新实测;官方 ML-DSA 真向量已通过 | +| Ledger 设备唯一 ID(官方 DMK Genuine Check) | ✅ 本地真实在线验真 | ✅ Nano X 早期版本通过;生产服务端需拥有 DMK 状态机 | +| Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | +| Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | +| app-monorepo 调试页(Developer→Gallery→DeviceAuthenticity) | ✅ 已加 | ✅ 已用它测通两端 | +| 账户名读取 · Ledger | ✅ Desktop 已实现只读清单 | ✅ 本机读出 Ethereum 名称/地址 | +| 账户名读取 · Trezor | ✅ Desktop 已实现 Suite 缓存只读清单 | ✅ 本机读出 Bitcoin 首个 receive address;不连接设备 | +| 本地 mock 发券 | ✅ 本地可信 background 拥有 challenge→ 验真 → 发券全流程 | Trezor 当前版待真机;Ledger 为真实厂商会话但不具备服务端可移植证明 | --- @@ -29,11 +36,11 @@ ## 2. 仓库与分支 -| 仓库 | 分支 | 作用 | -|---|---|---| -| `~/Development/OnekeyWork/hardware-js-sdk` | `feat/device-attestation-id`(从 `onekey` 切) | SDK 实现(新 `hwk-*` 栈) | -| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `debug/fw-progress-trace-6.5.0` | 调试页 + service 转发方法 | -| 参考(只读) | — | `~/Development/OnekeyWork/trezor-suite`、`trezor-firmware`、`node_modules/@ledgerhq/device-management-kit`(DMK) | +| 仓库 | 分支 | 作用 | +| -------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `~/Development/OnekeyWork/hardware-js-sdk` | `feat/device-attestation-id`(从 `onekey` 切) | SDK 实现(新 `hwk-*` 栈) | +| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `debug/fw-progress-trace-6.5.0` | 调试页 + service 转发方法 | +| 参考(只读) | — | `~/Development/OnekeyWork/trezor-suite`、`trezor-firmware`、`node_modules/@ledgerhq/device-management-kit`(DMK) | > ⚠️ 关键前提:设备认证只能加在**新 `hwk-*` 栈**(`hwk-trezor-adapter`/`hwk-ledger-adapter`/`hwk-trezor-connector`),**不能**加在 legacy `packages/core`(`DeviceVerify.ts` 那套)——legacy 的 wire protobuf 里没有 `AuthenticateDevice` 消息、features 也没有 `internal_model`。 @@ -43,25 +50,33 @@ ### 3.1 Trezor(本地可验,最干净) -**机制**:`AuthenticateDevice` → 设备用 Optiga 安全芯片私钥对主机随机 challenge 签名,返回 `AuthenticityProof`(证书链 + 签名)。主机侧**本地**用 Trezor 公开的根 CA 公钥验证整条证书链 + challenge 签名,从证书 subject 取序列号(OID 2.5.4.5)/设备公钥。 +**机制**:`AuthenticateDevice({ challenge, stream: true })` → 旧固件直接返回 +`AuthenticityProof`,Safe 7 当前固件先返回各 proof part 的大小,再由主机以 +`GetAuthenticityProofChunk` 分块取回。主机侧使用 Trezor 公开的生产根验证证书与 +challenge 签名。T3W1 强制同时验证 Optiga(P-256)、Tropic(Ed25519) 和 +MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 **支持机型**:Safe 3(T2B1/T3B1)、Safe 5(T3T1)、T3W1(+Tropic)。**Trezor One(T1B1)/Model T(T2T1) 无安全芯片,不支持**。 **API**:`TrezorAdapter.verifyDeviceAuthenticity(connectId, { dangerouslyAllowDebugKeys? })` → `Response`: + ```ts { verified: boolean, deviceId?, deviceCertPubKey?, serialNumber?, rootPubKey?, caPubKey?, usedDebugKey?, error? } ``` + `deviceId` = 序列号(T3W1+)或 `sha256(deviceCertPubKey)`。**只信 `verified===true` 且 `usedDebugKey!==true`**。 **新增验签模块**:`packages/hwk-trezor-adapter/src/deviceAuthenticity/`(从 `@trezor/device-authenticity` 移植): + - `x509certificate.ts`(DER/X.509 解析,自包含,原样移植) -- `verifySignatures.ts`(P-256 + Ed25519,用 `@noble/curves`,**关 lowS**——硬件签名非 low-S;ML-DSA 未支持,缺 `@noble/post-quantum` 依赖,仅 T3W1 第三层) -- `verifyAuthenticityProof.ts`(根CA→CA→设备→challenge 全链 + CA 黑名单钩子) -- `config.ts`(Trezor 根 CA 公钥,来自固件 `hsm_keys.h`,逐字节核对无误) -- `index.ts`(`authenticateDeviceFromProof()`:整体 try/catch 兜底、T3W1 强制 Tropic + serial 交叉校验、debug-key 标记) -- `__tests__/verifyAuthenticityProof.test.ts`(**10 个测试全绿**,用 trezor-suite 真实黄金向量 + 反例) +- `verifySignatures.ts`(P-256 + Ed25519 + ML-DSA-44;P-256 **关 lowS**——硬件签名非 low-S) +- `verifyAuthenticityProof.ts`(根 CA→CA→ 设备 →challenge 全链 + CA 黑名单钩子) +- `config.ts` + `trezorRootKeysMLDSA.ts`(Trezor 公开生产根) +- `index.ts`(`authenticateDeviceFromProof()`:整体 try/catch 兜底、T3W1 强制三层 + serial 交叉校验、debug-key 标记) +- `__tests__/verifyAuthenticityProof.test.ts`(包含 Trezor Suite 官方 MCU/ML-DSA 真证书和签名黄金向量) **设备调用接线**(3 处): + - `hwk-adapter-core/src/types/wallet.ts`:`IDeviceManagerMethods` 加可选 `authenticateDevice` - `hwk-trezor-connector/src/index.ts`:switch 加 `case 'authenticateDevice'` 发 `AuthenticateDevice` 消息 - `hwk-trezor-adapter/src/adapter/TrezorAdapter.ts`:`verifyDeviceAuthenticity` 方法 @@ -75,14 +90,17 @@ **最终机制**:用 DMK 的 `GenuineCheckDeviceAction`——和"装 App"**完全同一套** secure-channel 管道(`wss://scriptrunner.api.live.ledger.com/update/genuine`)。DMK 内部读设备 attestation 证书、算出 `deviceId = sha3_256(attestation pubkey)`,并给出 `{ isGenuine }` 真伪结论。**无需 token / 不用登录**(设备自己在会话里握手),装 App 能连=genuine 能连、零额外配置。 **API**:`LedgerAdapter.verifyDeviceAuthenticity(connectId)` → `Response`: + ```ts { verified: boolean, deviceId?, attestationPubKey?, note } ``` + - `verified` = Ledger HSM 的 `isGenuine` - `deviceId` = `sha3_256(attestation pubkey)`(后端验证过) - `attestationPubKey` = 65 字节裸公钥(best-effort,见下) **实现**(都在 `hwk-ledger-adapter`): + - `connector/LedgerConnectorBase.ts`:`case 'getDeviceGenuineCheck'` —— `dmk.executeDeviceAction({sessionId, new GenuineCheckDeviceAction({input:{}})})` + `deviceActionToPromise`(**照抄装 App 的模式**),从 Pending 的 `intermediateValue.deviceId`(Uint8Array)捕获 deviceId,**5 分钟超时**("Allow secure connection" 确认看门狗不会为它暂停)。 - `adapter/LedgerAdapter.ts`:`verifyDeviceAuthenticity` 走该 case;没拿到 deviceId 时返回 failure(不给假 id)。 @@ -91,13 +109,16 @@ **真机结果**(Nano X):`Verified ✅`,`deviceId=818706ff78239d1ab642df69c1d1fdd48acfc59ba282f4139383cfee31adfcdc`(走 genuine-check backend)。`attestationPubKey` 字段的真机验证**待做**(清缓存重启 Desktop 后看 Ledger 结果多一行 `attestationPubKey: 04...`)。 ### 3.3 Review 修复状态(Fable 5 审查) -- **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 修复:T3W1 强制验 Tropic(Ed25519) + optiga/tropic 序列号交叉校验。真机已验证。 + +- **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 代码修复:T3W1 强制 + Optiga + Tropic + MCU/ML-DSA,并交叉校验 serial;当前三层版本待 Safe 7 真机重测。 - **H2**(Ledger 假 id 易误用)→ ✅ 已被 genuine-check 重构取代:现在 Ledger 有真 `verified` + 后端验证的 deviceId。 - **H3**(Ledger `E0 52` p1/p2 是猜测 + 批次证书风险)→ ✅ 真机坐实"裸 E0 52 不成立",已改走 genuine-check(彻底绕开该风险)。 - **M1**(无吊销通道)→ ✅ 加 CA 公钥黑名单钩子(默认空)。 - **M2**(畸形证书抛未捕获异常)→ ✅ `authenticateDeviceFromProof` 整体 try/catch → `verified:false`。 - **M3**(debug key 无守卫)→ ✅ 参数改名 `dangerouslyAllowDebugKeys` + 结果带 `usedDebugKey` 标志。 -- 未做:ML-DSA(缺 `@noble/post-quantum` 依赖,T3W1 第三层,optiga+tropic 已够);L1(CA notBefore 未来检查,Low)。 +- 已补:ML-DSA-44、Safe 7 流式 proof protobuf/连接逻辑和生产根;仍待当前 App + 真机完整跑通。L1(CA notBefore 未来检查,Low)仍未做。 --- @@ -133,12 +154,14 @@ yarn workspace @onekeyhq/desktop dev ## 5. app-monorepo 调试页(怎么测) **新增/改动**(`app-monorepo-fix-ledger`): + - `packages/kit-bg/src/services/ServiceThirdPartyHardware/index.ts`:加 background 转发方法 `thirdPartyHardwareVerifyDeviceAuthenticity({ vendor, connectId })` → `adapter.hw.verifyDeviceAuthenticity(connectId)`。 - `packages/kit/src/views/Developer/pages/Gallery/Components/stories/DeviceAuthenticity.tsx`:**新页面**(vendor 切换 + Search 拿 connectId + Verify + 结果显示)。 - `packages/shared/src/routes/gallery.ts`:枚举加 `ComponentDeviceAuthenticity`。 - `packages/kit/src/views/Developer/pages/Gallery/index.tsx`:lazy import + 路由(菜单自动从枚举生成)。 **测试步骤**:Desktop 里 → 开 Developer Mode → **Developer → Gallery → 搜 "DeviceAuthenticity"** → 选 Trezor/Ledger → **Search devices**(先关掉 Trezor Suite / Ledger Live,它们抢 USB/HID)→ **verifyDeviceAuthenticity**。 + - Ledger 首次会弹设备上的 "Allow secure connection" + 联网。 - **Ledger 的 connectId 为空是正常的**(USB 单设备自动选中);页面已处理(handleVerify 对 Ledger 不强制 connectId)。 @@ -151,7 +174,9 @@ yarn workspace @onekeyhq/desktop dev ## 6. 工作线 ②:账户名迁移 —— 调研结论(未实现) ### 6.1 Ledger Live —— ✅ 可行(纯本地明文) + 数据在 `~/Library/Application Support/Ledger Live/app.json`(明文 JSON,未加密)。本机 17 账户实测跑通。 + - **账户名**:`data.wallet.accountsData.accountNames`(用户改的,`[[accountId,name],...]`)优先,回退 `data.accounts[].data.name`(已解析默认名,永不空)。 - **地址**:EVM 用 `accounts[].data.freshAddress`(稳定,小写比对);BTC/UTXO 的 `freshAddress` 是"下一个未用地址"**不稳定**,得用 `xpub`+`derivationMode` 或派生 index0。 - **account id 格式**:`js:2:<链>::<派生模式>`。 @@ -159,20 +184,24 @@ yarn workspace @onekeyhq/desktop dev - **`~/Library/Application Support/Ledger Wallet/`** 只有 Crashpad/sentry,无 app.json;数据在 "Ledger Live" 目录。 ### 6.2 Trezor Suite —— ⚠️ 难(接设备 + 云端文件) + 账户标签是**加密的**(labeling/metadata 功能),设计上不让纯本地读: + - **加密**:`.mtdt` 文件 AES-256-GCM。密钥根 `masterKey` **必须由 Trezor 设备**通过 CipherKeyValue 派生: - `path="m/10015'/0'"`, `key="Enable labeling?"`, `value="fedcba98765432100123456789abcdeffedcba98765432100123456789abcdef"`, `encrypt=true` - 之后纯软件:`deriveMetadataKey(masterKey,xpub)=base58check(HMAC-SHA256(masterKey,xpub))` → 每账户一把;`deriveAesKey`/`deriveFilename` 见 `trezor-suite/suite/metadata/src/metadataUtils.ts`。 - **接设备只需一次**(拿 masterKey),逐账户解密全是软件,不用每账户点设备。 - **文件在哪**:旧版/本地模式在 `userData/metadata/*.mtdt`(接设备就能读);**新版 Suite(26.5.2,用户这台)已搬到 Dropbox / Google Drive / suite-sync 中继**——本地没文件,得让用户 OAuth 授权网盘/连中继取回。 -- **解密后结构**:`{ accountLabel, addressLabels:{地址→名字}, outputLabels }`。`addressLabels` 直接是 {地址→名字};`accountLabel` 是账户整体名,按地址匹配需先从 xpub 派生该账户地址集。 +- **解密后结构**:`{ accountLabel, addressLabels:{地址→名字}, outputLabels }`。`addressLabels` 直接是 {地址 → 名字};`accountLabel` 是账户整体名,按地址匹配需先从 xpub 派生该账户地址集。 - **本机现状**:labeling 从没启用过,本地无任何标签(xpub 在 IndexedDB 明文,但没名字)。要真测需一台用户确实改过名的机器/账户。 - **可行性结论**:**(b) 半能**——接设备拿钥匙 + 搞定文件来源(本地直接读 / 云上要授权)。比 Ledger 重不少。 ### 6.3 迁移弹窗匹配逻辑(通用) + OneKey 侧已有用户账户地址/xpub。对每条导入 `{地址或xpub → 名字}`:EVM 按地址比、BTC/xpub 类按 xpub(或派生首地址)比,命中就弹"要不要把这个账户也改成这个名"。 ### 6.4 待你拍板 + - 是否**先做 Ledger**(纯本地、快)、Trezor 排二期? - Trezor 若做,目标用户主要是"本地存标签"还是"云同步"?后者要把 Dropbox/Google OAuth 纳入范围。 @@ -181,17 +210,20 @@ OneKey 侧已有用户账户地址/xpub。对每条导入 `{地址或xpub → ## 7. 关键文件索引 **SDK(hardware-js-sdk)** + - 新增:`packages/hwk-trezor-adapter/src/deviceAuthenticity/`(验签模块 + 测试) - 改:`hwk-trezor-adapter/src/adapter/TrezorAdapter.ts`、`hwk-trezor-connector/src/index.ts`、`hwk-adapter-core/src/types/wallet.ts` - 改:`hwk-ledger-adapter/src/adapter/LedgerAdapter.ts`、`hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts` - 改:两个 `package.json`(加 `@noble/curves`/`@noble/hashes`)、`hwk-demo` 两个文件(web 版调试按钮,可选) **app-monorepo-fix-ledger** + - `packages/kit-bg/src/services/ServiceThirdPartyHardware/index.ts` - `packages/kit/src/views/Developer/pages/Gallery/Components/stories/DeviceAuthenticity.tsx`(新) - `packages/shared/src/routes/gallery.ts`、`packages/kit/src/views/Developer/pages/Gallery/index.tsx` **参考/证据** + - Trezor 验签参考:`trezor-suite/packages/device-authenticity/src/` - Trezor 根 CA:`trezor-firmware/core/embed/projects/prodtest/cmd/hsm_keys.h` - Ledger genuine-check:`node_modules/@ledgerhq/device-management-kit/lib/esm/src/api/secure-channel/` @@ -201,6 +233,7 @@ OneKey 侧已有用户账户地址/xpub。对每条导入 `{地址或xpub → --- ## 8. 未提交 / 未决事项 + - 原始调试工作已分别 checkpoint;生产接入工作在 `codex/device-attestation-voucher-proof` 和 `codex/third-party-device-onboarding-rewards` 分支继续。 diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md index 931f03fa3..0c5909d0a 100644 --- a/docs/device-attestation-voucher-backend.md +++ b/docs/device-attestation-voucher-backend.md @@ -247,7 +247,8 @@ Ledger 客户端提交的 `verified`、`deviceId`、`attestationPubKey` 只能 3. 使用后端内置、版本化的生产 root 集合及吊销表; 4. 验证证书链角色、有效期、签名算法与 nonce 签名; 5. 从证书推导型号,客户端的 `deviceModelHint` 只做一致性检查; -6. T3W1 强制 Optiga + Tropic,两条链的 subject serial 必须一致; +6. T3W1 强制 Optiga + Tropic + MCU/ML-DSA 三层证明,三张设备证书的 + subject serial 必须都存在且一致;缺任一层直接拒绝; 7. 不接受 debug/staging root; 8. 从已验证证书推导权威 vendor DSID: - 有 subject serial:`trezor:v1:`; @@ -259,16 +260,50 @@ Ledger 客户端提交的 `verified`、`deviceId`、`attestationPubKey` 只能 Ledger DMK genuine check 的 secure channel 位于设备与 Ledger HSM 之间,公开 action 只给出 genuine verdict,不能把客户端布尔值转换成 OneKey 可验证证明。因此: -1. 后端创建一次性 relay session 和单次 WSS token; -2. relay 只允许既定的 Ledger genuine 上游,禁止任意代理; -3. relay 在不改变字节流的前提下记录本次会话所需的协议状态; -4. 必须观察到同一会话的设备 attestation certificate 读取和最终 genuine verdict; -5. 设备公钥与 verdict 必须属于同一上游连接、同一 relay session; -6. 会话完成后立刻失效,不允许重连或重放; -7. 权威 DSID 为 `ledger:v1:`; -8. 上游断开、协议缺失、无法确定 freshness 或 `isGenuine=false` 时一律拒绝发券。 - -relay 方案上线前必须完成 Ledger 协议、服务条款、freshness 和重放保证的安全评审。如果 relay 不能可靠建立这些保证,Ledger 活动应保持 fail closed,直到 Ledger 提供绑定 OneKey nonce/audience 的 JWS/COSE receipt,或使用具备 endorsement attestation 的专用 Ledger App。 +推荐实现是“服务端拥有 Genuine Check 状态机”,而不是客户端运行 DMK 后上传结果: + +1. 后端创建一次性 session、challenge 和短期鉴权 token; +2. App 只负责 USB/BLE APDU 转发,不解释、不修改、不决定结果; +3. 后端运行 Ledger DMK `GenuineCheckDeviceAction`,其 transport 通过上述 + 双向通道把 APDU 发给 App,再由 App 发给物理设备; +4. 后端自己的 DMK 实例得到 `isGenuine` 和同一会话中的 attestation + `deviceId`,并把它们原子绑定到 session; +5. 会话完成后立即失效,断线不能续用,APDU 序号、方向、大小和总量都要限制; +6. 权威 DSID 为 `ledger:v1:`; +7. 上游断开、协议缺失、无法确定 freshness 或 `isGenuine=false` 时一律拒绝发券。 + +另一条可接受路线是 Ledger 向 OneKey 服务端返回带 nonce/audience 的厂商签名 +JWS/COSE receipt。单纯透明代理 Ledger WSS、由客户端计算 `isGenuine`,除非安全评审能 +证明服务端可独立观察并绑定最终 verdict,否则仍然不够;客户端 +`{ verified: true, deviceId }` 永远不能作为发券证据。 + +### 4.4 当前本地 mock 的真实性和边界 + +本地 mock 只替换部署位置,不替换密码学验证: + +- Trezor:本地可信 background 生成 32-byte challenge,设备签名,background + 使用固定生产根证书重新验证原始 Optiga/Tropic/MCU 证明,验证成功后才生成开发券。 + 这段 verifier 和 evidence DTO 可以原样移动到后端。 +- Ledger:本地可信 background 直接运行官方 DMK Genuine Check 并连接 Ledger HSM, + 同一个函数拿到真实 verdict 后才生成开发券。它验证了设备、SDK 和厂商网络流程, + 但结果不能作为可上传的服务端证明;生产仍需上一节的“服务端 DMK + 远程 APDU + transport”或厂商签名 receipt。 +- `runTrustedLocalMockDeviceClaim` 接收的是执行真实验真的闭包,不接受 renderer + 提交的 attestation DTO;challenge 在执行器内部生成,领券前不会交给不可信调用方 + 决定验证结果。 + +### 4.5 平台与联网矩阵 + +| 厂商方案 | macOS / Windows / Linux Desktop | 浏览器插件 | iOS / Android | 验证时联网 | +| ----------------------------------- | ------------------------------- | ----------------- | ------------------------------- | --------------------------------- | +| Trezor 本地证书验证 | 支持,USB/BLE 取 proof | WebUSB 可用时支持 | 有对应 USB/BLE transport 时支持 | 不需要;拿到 proof 后可完全离线验 | +| Trezor 后端发券 | 支持 | 支持 | 支持 | 需要连接 OneKey 后端提交 proof | +| Ledger 当前本地 DMK mock | 支持 WebHID/BLE | WebHID 可用时支持 | DMK BLE transport 可用时支持 | 必须连接 Ledger HSM | +| Ledger 生产服务端 DMK + APDU bridge | 支持 | 支持 | 支持 | 必须连接 OneKey 后端和 Ledger HSM | + +操作系统不是主要限制,真正的限制是该端是否具备对应 USB/BLE transport。Trezor +One / Model T 没有本方案需要的制造商安全元件证明,不能参加“原厂设备认证发券”; +Ledger 没有公开支持的离线制造商验真流程,必须 fail closed 地依赖在线厂商服务。 ## 5. 数据模型与事务 @@ -416,7 +451,8 @@ AND postAddTask(walletId, campaignId) has never completed ## 9. 上线门槛 -- Trezor:challenge 置换、重放、错用户、过期、debug root、错链、缺 Tropic、serial 不同、畸形/超大 DER 全部拒绝; +- Trezor:challenge 置换、重放、错用户、过期、debug root、错链、缺 + Optiga/Tropic/MCU 任一层、serial 不同、畸形/超大 DER 全部拒绝; - Ledger:伪造 `{verified:true, deviceId}` 必须拒绝;relay session 过期、错用户、重放、APDU 篡改、non-genuine、缺 certificate、并发双提交全部拒绝; - 两端:地址签名与 challenge 不匹配、活动重复领取、事务中途失败不发券; - 名称同步:只在首次新建钱包触发、只改用户确认项、加密/云端来源不误报成功; diff --git a/packages/hwk-trezor-adapter/jest.config.js b/packages/hwk-trezor-adapter/jest.config.js index 0016f5a36..64695eac8 100644 --- a/packages/hwk-trezor-adapter/jest.config.js +++ b/packages/hwk-trezor-adapter/jest.config.js @@ -1,6 +1,7 @@ module.exports = { preset: '../../jest.config.js', testEnvironment: 'node', + transformIgnorePatterns: ['/node_modules/(?!@noble/(post-quantum|curves|hashes|ciphers)/)'], modulePathIgnorePatterns: ['node_modules', '/dist'], moduleNameMapper: { '^@onekeyfe/hwk-adapter-core$': '/../hwk-adapter-core/src/index.ts', diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index 14381c373..6c4479e5d 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -45,6 +45,7 @@ "dependencies": { "@noble/curves": "^1.9.7", "@noble/hashes": "^1.3.3", + "@noble/post-quantum": "^0.6.1", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.1", "tslib": "^2.6.2" diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts new file mode 100644 index 000000000..044a32bb8 --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts @@ -0,0 +1,8 @@ +// Golden ML-DSA vector synchronized from Trezor Suite device-authenticity mocks. +export const CHALLENGE = '29d0be0f3cb191c80d108359c64d22984a77ad8b99433814be31db0b6e9e7920'; +export const DEVICE_CERT_MCU = + '30820fdf30820655a003020102020868b1982d44540915300b06096086480165030403113054310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3125302306035504030c1c5472657a6f72204d616e75666163747572696e6720526f6f742043413020170d3235303832393132353934375a180f32303535303832323132353934375a3046311b301906035504030c1254335731205472657a6f7220536166652037311730150603550405130e3437333032313932363030303972310e300c060355042e130553544d333230820532300b06096086480165030403110382052100f71c837210b55caed1d1a401f232931ee892808bb4bbd186175e54b0fffe27a15494e5b4f548194e7c099689df4c8b207bbaa6e8e26e457581988d09fb85f5896ac13cc2e5832963099ab5dc844cdd5d3515abe7c9a2253aec5e6e98be9f5e1b8000b5c5cc52a3985ef8e0b94525811d8e31f8ea8f20d359d0b81e174a7da937a129606583f1321bcdb5067c45af60e13dd5d32ec8a328ed724882dff570c11a7bda15d8cd5995c25261887cb64c830922618df5be0d5bbd5b8a93af1baf677299a6abb986f1e88792fba0a22b223729e1a05cc55a53eb4d88ffe111aaa7cddc97764ac9438133e52528ebe622aab63ef921fa689cfbc5841fdfd8c6c8aaaf52cf55f0d6ce13e26e593bec6b57a1ef9133a6d98fbfaf630abb91a58ec27db450f31339d4ec1d3e9c1870ac8b7abaff117b41fa5adebd226e1f5326d1c06920664d71b633bef50542eee4e518ba478abafc4d539b1c9799183bc83b95bf5e0ad1df9dbd7160c053aad0ce825e5615083c0cca262a552d0b1ecb3e08d46b5364403beb1182155c1421f79bd6c53a55869bc81928b93e4902b9546fa76e47d7cb7ccb11321788e10cd354b2c0a9f4e85cfd0cbcd3827bda7e2ce9930906af9cb0ea43612847183fbcf0fea73109fbf164ae3257e02119d53614e0e4257c975c6fb9e247293b1abad833c8e16bded2f151d2dfd5e98cd15fcb930836b66a74044223715bc7e540785bd45bbbae59c7a9520ce03747f4dcc5faeeec5f99c21f5b6aa0b985e7f0690e4c58c15b32003d8a2d4c0075e1689b814e0855ab418e2853ef28f8e7e4619ad425c720ff7315d2a311761bab30c8ac9708a539b35349559b946310a7889fb479e451f396125d48fb246346aff8577e26d1ea58841ca35a6dbac02cf568d5395c399aeb44b18c6e87ceb7811bf34619e90585bbcf64521a4fd2359c5fd923b9bffc3fd423d8ce0712bcc7d7747f6d6542125449afa75adc154a758578f1cfcd8751fdf2a18c516eed70fe0b196d7917b2934dce7a3e67d204bbb4d1430824aa5e803675319715f58367f2ac7e829bc314f1f87ccd78f5e226d66d2d8f031c60fb592323d10282ad24673f6498d643f0d5fb348e00ac94d3352a6c015df8c38af4d350e4913020171994637f4486ad8ac9928582142c983c4653da7c4a2275a3083ce9cc18fef70890eb6ee91067a18722aacbbb50b43050f7fdf1721ade8c0908d05686c7db0a7c2e63c997afbb85c34eb8a0666e382c635b49c301b12887fe96a9d266ff59be7d75ef43e2a583fc012034fa32e06241bd1d24a3f8a9ddeecacba68fd679b72c50c46d9ce1f757e83f94cfbc49069bc4137bf2384ad5c2c5cdea947e64c47cd5382a0ec9e05f102152b4d8ee4fae971df142ee29ee0b094866ca7e5c4232f63e1683d6406ad43178d9ae0360c8ef43d690ab6776f04efbf9704b4e51c228ba4dfa7d58db1b9d5a1526559c4c6c0531e9fc3531718142df0de39539f1f45bc60df753e8ccddab5c2874b4f1124869c21a7701f92fcec0b3d56d79713e934f6617fb2295b29a2bc3f90e20346547ecbe1118a8f86580e310ea6b40e7445c12e1ad7c2ad11cdf090e7d00fd49c56a8b23f5bd3d01a2a8f4b30d619d57dc2158b3a2ef0ba51878e414c7f1da5711aba389fa9944ae6f1012f24a8e9b0340cc1373339a05c438983660a1525fcd93a60011df2d3749e2e19f0096bc0af765c5bf350bb414ce6797888f470624444f1d69eb8d8d4f8f556e3d3e1f27a3325633e3560c489f9aad22c50c09b7f759bbcf44eaef83389adead48884e2a9d865a54fb21d75f2c2a6fb67a7231d69a75e1ab7e16f11fe91e26a341303f300e0603551d0f0101ff040403020080300c0603551d130101ff04023000301f0603551d230418301680140bc62f01e47558497cea38acf5d4e8a8b673306a300b06096086480165030403110382097500911e8268e98e6ca111577c697ac0938864b0480c0045e01838033624ad2956c40917ec55b992d7c0ebc2945b37a4338cd7bea024679281d14f03760ab7fd66fdf3b5160f4e3164e6a19fdd414e72ee2ff01d6a4ebd51dda6321bc12f3e6d3f3d6082b1c94db2a73aab60857ea8cc97f825abcc8fc98dbb053631bdc7fd028e5bc4639ac5277b1415491b4f77830af6916f312646f25973dba75e8d77c06d9c71c5a4bf31c3c5697213fb396512e77316ab0ba7018eda7f140a1ae5c5826e263a581e1eda33a6ab55a57252f7088c12a47f5396df72f1f6911d9ac76cb7cb7e0a0f0015b0f49032015a14293a2a383e475af8348578dcf82868afdb85942bf5d7d8c349aa36691f95ab1b7fed97b9fc14a7bbc2c2d802b66e12236f221f3d39b57f9cc73285663ee2302fad3b012372d64ef83f9bec4ff1e8269d27f024f56ac1bd86dd11bb76fff9c9fae9944fbefe20fa8ad307855bf8d5f8e07d389de3bbdf0693f57df7616ca6f5b2037b20f03f09706b20c87fc8d2cc344f42ce1cef6d757640f8d1c45e7e26a66af4ca29f3f37003e2926cf6216acabb01b58d367f3e7f1bd409c0bc21d2e4875bc67608039db6fb314f990cb5115301994fec65f92cea6090adf930d7754b81451db6d85bb98b9f09e72d435d58d05a3b941c7069acc55ff8a4670696c791367b2de84c34435d17d65125e5b2d31704f5934863ad5a43deef62cb3cc8314b24ab1d33328bf036a735d08022198960f3a7bb7da1c740cdd082addb78423162e00dc3d46bec006d0d9bd972d1f2822351e8c3b4ba0a4b34efceb2aa07e27d9ee19e861ab87cc4a4ab0ccf1842053e0fc11d9e792fcf642f7c22f6cfa470c83c7eea66db066bb99bb0e313c6419a7d3a6933d650d2075b553cd99d4d1a773eb3c6611b013a7444a01d89bb59b76c92523efbab92399fd1c1820b0a46e413f5fdb7553776252543cea685e31adab2a2499149e6e8bf7779924d81c2714aab9e2d10995a07220fc8b69bd8188819356329a74fc782baeec84f9e5cfa7325fb47008f57d04b10d652bc1b5e317dfa71a4c379b07e636bdcb7a944920ddb4523a50d1f0b8e8735cc3ceb33c6c8c42888040e8e909dc8593edad2d6eed931a9881e662c72f43f837ad0274f8845268d544ac59a8d797075c761fcf4bbc892d4fbe031c70097f82157abae5eca1b0559828e422564a3636e6621cf40d0a59daf6738cca3c5ef1e1c51d9f2711335b6946a709c0fb131f4dbd8f89dd2e72d4a4eda83f6fb84f86569a9142a60f26757833aec49f015f18e73f142ea790af766bddbff3a3337ba6fef219834a597a6651bdac2e741e75ace4da1a934fc28e183a497682fc80ecc0e41a43edadcfab9d428ff10088abbec78889e855bad736bc0480d57231d5672ebc55ded6b4aba8a2fda397351f5ffe7d448b1ba55439d6fcc51110b76ce34e27fdaca5bbcd28bbda386ba47853a9e978a93aa3578babf398e67f30e47f18abdd9e4dfc89d66a8017a26417ae83440098ba749ffde54aba8ccd359202b98f6ca041f19b1df6b239d572202a4ca6fe98980bf49c8d3e39b7ce98f0730113720bf52b71005fea87105dd0364b004f734b669e6dad130327385c706c5004eddcbbda177b272319b37e32d82e873b6b5ec56972fa86c1f3555d3f62d8ef5cbc45ef64c31a21c819f16d1a0c985e080541f5fdb610efb5e2ff8dc17206b827feb646093b2aac95f1bad08ee3a0fbae40749c34da4188909923adf6b273437a102167c4a22d7b11a7c5df6876590261564825cdfde09cd87a1086cab90c786d650bced7e5f060de4e78007d7de9015a5887aba4b51be34b945f2f75d36562b9e8042155a8138f6a93d561fa4fb33b8cac57fa6627ba93ddf540ddf95c5430b1d539113f844c22f5378a1c083c093143918ffc0758f2ebdb7ecac11858571d9a3a03c833a7a834bc556409b80e295a83f825c728b72f59a12e68d840271544ac7bd48c40f8291ae2d96458eb7e59f720a8e073be1558d0b942438dab7f9007a6f1ed4ce55ce07ed28055383143ac7a61831c10d01c04530373f2b4cfe478f0bafc7d49dba637986c9af966a957eef5c15e203d192bca08521d7d4a234e0ea4ba1605ea7a98c4756fc06813ad71c5932583f61dae91f512a74ab1b4eeebb88fbbe9256d041fe8ea7ff15d757d9e07e6d0cbd38bd0cfe851bc60a616e6e7feec9d14e263e69b6e499096dd28fe1655f6032fc39678ffcd8002b1146dc4af90be09ba1f0df5effe172d79e5eca8b519dfc3585a326bb1a8e970fe103b5e44bb2e9b217713e8d02a9131ddb3bb323139423c62ae32224fb6da9894c397e4a4d88868b242b08ac7643ed9c381ed83c1a131c7d53cee7da94064f2c2866652827310f96d52a6bf341f9374e5b48bb3fa520abee80cb2d1906a8cd5ef070255297004dc389d2d4739d0f5741d234aa595c7cea5274d732f0c562e14e4db08024380b2bf131bba75682dd378d136e2e8a1a29a521af5f9631a14d09c5d9965b9e7d3151d0ca3f638ba6c16de16762d66ac2dd9d27abd588d98af348ee53dc787a7bd16e9110adfe2ea2f203ce15eed545393f0e7d824ad925c7ee4000bb3fc3c679b13702cb789e27428fcf6af0462799303d24b2b4dc5212ed356933c52077eac0927b4c9580e78cd2fadfcd05789b664ec40ddc92cbc19c2911610a5d31eaa29ea8ffaaa63b72cc01b4d78657c1efb23a711f072e5708938f2270f6e9e35c0de97a3155ac7b855110ffb6eac717ba24b8edffa4a2970b9bf9bc8a84ce3959341a921f6477a4004b90797a97c6eb3af5b73a3c5c777388bb667f8f00829df7032c42e8a279cd2986b9f92e997c3fa9ea2d299120ba7b0e9e3951c50c55db9570899b0076f157c2242602b83c2736951ed2381bc74ef84b52de3736e60c92bc978dca9155c5c2f25d409a5994d1d9df95d580e163baa4cd276434296f0a3e5286d9ceeaa1d8834284366b7b10b80c341a0d24608c6a09e3e09b03db789e3f8157e4af90bf581b833b1fae12c63335ef58787600577e2fd6f7363dfdd79fd460abcc98792a40e84f2000a0d77c91e95cbccc671e77d06a69d27e682cb4b50e2f94d6f5f41ff67cda3b1da86efe0c0f9fe38b247ca7f1a8f4e6c44aaed96679b7a1a20567e07772247876f21d598b820606f73c0860bf7410194675fc65b756dd7ffa9b040dbde30678cdf074b8f679739d88dffb95f010502502c9bc5ddae593a1b784efe741561a4b744da51505d7b77ae79f825863eef160c2954e4f081f282c404f515b7379869ba0aab2b9beccd0d7dff6151e243039565f8690c2cfe2e6e9f7042f3b4f5f71788182a0a5acb2c6cbcecff510181c4a57646586a0b4ce000000000000000000000000000016253742'; +export const SIGNATURE_MCU = + '4a48f6a40108b2ba5aad73cc09ecb304430c8ddd7b8170d1f16007de8e2f1c650011fd507fb0d24b29382c4c46d629502bfdfa5db8bc64d508f2f183a76696c473063daa906d55c3fa4d2906fa1a0487a96f5090f2ba80a90a8b7393ac761b6bf874f1dac821b6751c9865609d5541f8c59e4bbf4b8bb643a3da2c083fcaf1218f3e32bc6363ee681a3266eb09928f591cf3ed22ded28664eefe0effb5b4f21c5bc6f7af4b506999b35d4617dd98cb08003330d486bcd65d0ca335e4a3412db59095e7a0d6eb8fe9d6228849fdfae568f5fdd7338812264f9c750ecd07f42649e49e59015127f9249f334f2040f7bda1428a0b0d1c911e80d4e78dd4717820827f1248b4ee4127f6e3cbacf0eb37239db80d41a713db0d6706b8b541f18bd0e1dfce69b67e91ffb7630f5b5bf96c363ecb867edd396a3686c7d6282e336c2d3142168823b7ec1a71eef6f3897541d5dd539894b98fcae4b7cf5f10a90d2965aec9492fadbf49d1cf94ef308efd95f8f78dbc90d92ec402b9da5d15359972e74d60491882febdf75ffca2db289a02a6ba020a7ed2ade1cf0555df7ad52964754d0aaa1957caa37c50be67f63698461166042c8be25e41515bf9b9b1d6d7b8ffbf275769bc42bcdc9d29c973f15f3c10c0d896c3ee171fcf86b3939178c4308b1ccb2a620557caaf1941b3afee6958b53680be9d74ce89e5fff17e172db6b76d78c331a6729b7dc892a3001975e30f3839b9782ac88e43ec4ca27c3c297af88edfbba9e4c5a9bcb4e3f115cd9f7617a3f7300641e023541e33e345a5aa13dc32d38853d49270c7d54b2e1e81914e2cd507d288b34be7f6c80d27649f7e428ecb29a5e686081185a6de9fe571dc6fb349cf9bb24a57d4a4c8d356f0cf0a53be2f55f5c3743d140b267e11bc22e90a9483086850e9d75882fe95fa5973cc10a8e98c6c792f5671311a57e630ad1cd9462e9a39949214d92adeab7eb0272624c72e068c2292219fd9defc0a206a56e4801fe31774a18fe836892316eb720cfbb96ec144618e72ba556177e2a38fb685253afef2b0a02d4749492c5ccc9f529014d6d77ac33c5653bddbec3e471c385aeae69952bbf783e2bc38b0c3b0e6dc910b228b83164411cd65b5c98ebee3ca4063de362a159b43348fdc49f3092f4b4c9521ab4c8ec86a1067c12020181ba5bca6b60dc2e3b34c67d67d33c8603358d0d4dda666fb3a4682de2f7fbef3564cc3e75b26fb89222696f9bec3e90e9f62ad8649595165017e2ce7a267822609c28126a324e9045974078943bd21a35097a4b1dc968db25b66c7e83e832a37bdf664f5a478b3a366f36375762c34e3ff828aeb9c604bd82557007198d3a3fa09f8b569b31451e890271fe0356d4e4ab1cb9ad21d9fb713e95d0342fbc945d46e74379644a02b963c9ace3a5904015a7413cbaa11b27c24c77c8cb0345ab9685801ceb249b8ffa666bb87784cfb0b638fab84c3728790dd0293d7b1d1c877518c27a3a14ef57d4ac9e6f34ed3f4933222574833100bca16a7b0d72c54a62c40e0fda92ba025fbd6d9fb46fcf4e6144f680d2232189e307e496a9583d5822027a50ea11dfb8a8e6c1c85df00ad730d4d5fb32969e7bf06dcef83e5284ca9680894a7530c4ecb20f6de8c63824774ce9227c96fa0182b972869ebe23f11fe357c5d39d7be65c24be900e28aed940912c01770e66604968a5313cc24a47a31dbefbcc2ab208b4b8f4782d588d57e757dc3aa4f9ab4057c58977a0ac13c422e2b56eef39f8f08f6b19bc25cbee47a4841114d537fa3d2e8a5dc0a96b49b39ccf9fe7a587a02860cef30477d4398f9cb353c499d3751f73a7b955b0f89701700af3f0eee742150b9f9acd4520d316a97dcae11b705cb619582da1b621efb91bcb2c6e4ad0518e1b03e4b4a4bf12b0af7c630de012df80061a5a0ee12fc022e4532b489a7b0cb6012a25a0b3f158711c500dbaddfbde6e5822878b527d9802b274b2a9bb13699e3739ba0216dc40b798adb4789259d9f1212e07c4755595a91a2fd7cc56aceb528530bb21b5116b87dee47950f55c4a1d4f5b235eef954ee6bad03ce32e6829872de0cc9c60fb6dd252265c56b793b643b926acc50156322ad591898e5159555c9536ec47ace674916a19f19d9a026ab77662e0fd2dee3614ead82514054ccb1f810589debe3188d5e9c07bcccc943f1146a221a65f0a2842552529aaa2255fedb39805e4c00f3f8cec3966561f827aad0ebd41598c61ced9274e50f6bbc70c41986e315df4121406842b3856c4ec0c0af7e189142082608ebc5a1f2070afe4e88a4d782efbc01533ddb606bd6d2415b575f0027cb870e339f212e80a5d606ec3d416c37ddfeadfdc48fdffa88a8ebcc541d381b69ff51bae5f3f23e463196d0f9b03f865910028b4b32a9b114b130a2a9499452b9c2b6e00fce1b3c9514a5f9e1910d0aa783b5c0a8337dc114d87402d278c659afb4127ea9f09dfe6599b8ee4dc19d17bdbb40801f8d2f27fa2fe97d1a16a085c33098f023efa9428c3cbb897facde4c71f15874712c054a5f30c21f1f376529476b8bfb48a3029d8e9b684ccae66ca7c743c2c58cd4da5e8a9ac83bc9058c5258edfcafb1b6eb51b62b693f37ea5a36ce375d2cd6333868231ab7f5ef507089dc8f0fae8ffc5fdfee094c3e3b20c9eee1f677de869a0c5d44de148acdffe6b98a7ba5e7fdbd5ae4c2284d756135e8aa08fe912cd343e04cf36a43a24c97ff162480e660ecbdd09a3e77dd674c2e4949a5a87f93a98a209bdc37b94beafb2f911d2bde7a59685039553f6e2e44cae79ea766a53819926eec81070a988a234b45b50633158f890f5595c11c3162cef97c2cefdb439af0f86909091ea16590b6bfb20a3d23ea86ad04b11b7d4351ba983926664c3b0d75ca2c1f79e57b4a30bb8243fe7fdb773cadd9b954a2c8fe54d1d0196292367a3ba1396fcb59c4c3fd3301ded699a6144124e14b2a737b334cf29d3e9b644f3971b7defdf70b88bcec4d3adff7b1d93f5c77ae8971190d27ed8f43c5970d59feb4eaa4ffcf2c2a5060e78f697b760cb290b5eb6eebb1db7c7d7ff62584bb0ea1a806c1e06eb525baee7447e4efbbfbdc3b742def705f66d4fba9d4c74a33eef2f623eb34d6312396650bb0df2d82aa587c61a32860757bd8d2a7ecf037d542c9a2fcbb7c1b845013cbf0663a27f201a33fa2d0dfea978ad3c07c0d5d8df2163363d59ffb11d616d95103058b6cb52eed6d2f00421e63cb14b68de182a39d04d9ce909ee8778d037b3920356a6d909eaacad5d8e5f40f30373f4c777f96b4c7d9ddf00a0b232d2e36424f616c717898a9afb6bac1de090f20494c75777c838a8ca6bad1e9eef3000000000000000000000000000000000000000c192c3d'; +export const T3W1_ROOT_PUB_KEY_MLDSA = + '1f3e5edad45fc6980dbfd597ed17b77e4d4ad1b1ec4804e04dc061cc2cfe77bc96fbc93f698f1fe00c47177c8150276dab86cbf407add7527deb09b65a6623ef581422292d77744be64f4401ca65b9bd027e32a76f307413b086869eccbcfc5e8823be8e0b3897727bca568feeff62957cd1e261e4aab67271725d240c7e7d2a20a5c201699be83d6c6a57fdfc09ac9b6b599e2a14a50e421d246d1d1df805db0b25534e500fed76fa89975a4270e274a9fa72b528e47d999abdeca5e08e51d447ae4019f699088d485bb52d5eec7a8c725cd34ff955c16437877a131f068438b5699c2285000463e24c633b023cde279be015db2f8b16de07fa82ee9adcbf868b898e25eeef5d60d03e1da874aca6f2089b1e7c72d7b532d3a27e38936d53c0080f9908b6727b13fe961ec8b08004f077b9cce95b5eee9b44d351e79eb222aba1bc18895d36cf4843ffcdfc735e1b66f3e72db5aaf8b5ecf56dbf0d616cf53d192a60e61ad3c51a595213100fd00835434a28aab3eb5949d3405de5db1a586bd6e937444913e9f1d4c00247acd49a2ed9953636e5d74417b4c7144c6c22fd8f7e25cb6cf6c8510de6915b74c95a28c197152208096d0249b86150e807978ca985c110d07b7d0a24434039f6ef12a69d3e073af6ae44e7c4606a354a3e953bdcff10c8dc68b807f41ac563ff0d7141c328b34152a25a16e9cb8228f7707356d2d7f3caa8d778a85e3b66242c124281c0f3201ad28596b868db138149eb98b821dcaaf86b5e11c2ac1161975bf0b2ecefc111b7b7401ce261911526618044d126eeee885efe51be0e32e93aaaffe34899df5b10df7c5a9c54c5b4704671640a92e8631f0d401f44c2238b5f9a37499b3d83c1c60096f2b48c3081dd9769db325c1cd1ffe84bfd8a80dde3cd650c49e14af3ab6c317ee74bd39e976ff5b7ad490e80f3eff9edf3b3855070a5a06fbe9cbca54f521e3d74cf13a4d116e23e531a7c70232c044b7e7d02467afe2a40def14298688eced5d3d87a102d31d7cd5409918ced4bab190505df48f451deea1c29168246d621529f0346eaab0e2302f3ce206dd84837704b8bdedbadea5b5822cdecbfdb180cde14e62ee26a929f0a37a99bacfe237376a3e6c289b06b8e93ed9d1435e3ee6399ab1a06d4f70097f80e26fe6f60b27a68104f6333b8db1bc78e12d54647b23feb0f11ae070f3a6be9d9f4d89a6b5ef8595d010d410e72138cfcbd5e41cca1df33ddad657b699cf4d3dc602888e5aaa0a7a2325ba4a36d7de49c591d8e355fd1de644d772bf2a060093743388dbe5d30a2d97304abc17c547bd65276291d6aa7b0723703733239fabef39c2de47071d50074e68fbe71c88f75d4510bb5f6bc244b3e7b219eb89fe5f148459a2d693cd3fac32aa069cbdd4e27c9778096f6805bf6df9aec9671f0a58b28704f74775e81ac46826c1c4709b0e0989d6e8e73bd1795ce85d85c14ad2997cd2810272a5682e7ed9aa7a092708c0f4bc67f68d317d406a96ddd2842ea22cbf3410cd7a77005b61115b794894cf9d10ebb4f5552bec8c21d90ba97d6139d5261d7f51f4009c3618f9feb22cc929ade204eca7c0f813409ceb88a13512f85e03948314ba9c4488aafda88ce447a0e052a6a605e32d04a20e2e52c41f371247963f24fc28c5cc1ddd8fe1681a2a293daf2688e9b026303a76cc60f40290c4d823efb85f36cb5e092369cc20515a8077e7702ed54ca1565d37b83184ee46c4a6adf2840f5a5fb002db70690bbf93888e685fdd1f9ea211b42693e0bd5ba5235a6ea1b714774d13279d7db3440f7fbced53f203ce6500555bcb9251e6f36d3352326501c'; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts index 11fb5b522..39b70d9de 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts @@ -1,7 +1,15 @@ import { authenticateDeviceFromProof, getRequiredDeviceAuthenticityLayers } from '..'; import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from '../verifyAuthenticityProof'; +import { verifySignatureMLDSA44 } from '../verifySignatures'; +import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'; import type { DeviceAuthenticityConfig } from '../types'; +import { + CHALLENGE as MCU_CHALLENGE, + DEVICE_CERT_MCU, + SIGNATURE_MCU, + T3W1_ROOT_PUB_KEY_MLDSA, +} from './mcuFixture'; // Golden vectors from trezor-suite `mockDeviceAuthenticityData.ts`. // These are real device/CA certificate chains and challenge signatures. @@ -95,11 +103,36 @@ describe('verifyAuthenticityProof', () => { expect(result.valid).toBe(false); if (!result.valid) expect(result.error).toBe('ROOT_PUBKEY_NOT_FOUND'); }); + + it('verifies Trezor Suite’s genuine MCU ML-DSA certificate and challenge signature', () => { + const result = verifyAuthenticityProof({ + certificates: [DEVICE_CERT_MCU], + signature: SIGNATURE_MCU, + signedData: prepareDeviceAuthenticityData({ + payload: Buffer.from(MCU_CHALLENGE, 'hex'), + }), + deviceModel: 'T3W1', + config: { + version: 1, + T3W1: { + rootPubKeysOptiga: [], + rootPubKeysMLDSA: [T3W1_ROOT_PUB_KEY_MLDSA], + }, + }, + }); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.rootPubKey).toBe(T3W1_ROOT_PUB_KEY_MLDSA); + expect(result.deviceCertPubKey).toBeTruthy(); + expect(result.serialNumber).toBeTruthy(); + } + }); }); describe('authenticateDeviceFromProof', () => { it('keeps T3W1 Tropic requirements independent from root-key configuration', () => { - expect(getRequiredDeviceAuthenticityLayers('T3W1')).toEqual(['optiga', 'tropic']); + expect(getRequiredDeviceAuthenticityLayers('T3W1')).toEqual(['optiga', 'tropic', 'mcu']); expect(getRequiredDeviceAuthenticityLayers('T2B1')).toEqual(['optiga']); }); @@ -195,6 +228,43 @@ describe('authenticateDeviceFromProof', () => { // even reach the tropic requirement; either way it must NOT verify on T3W1. expect(result.verified).toBe(false); }); + + it('requires the MCU ML-DSA layer on T3W1', () => { + const result = authenticateDeviceFromProof({ + proof: { + optiga_certificates: [DEVICE_CERT_TROPIC, CA_CERT_TROPIC], + optiga_signature: SIGNATURE_TROPIC, + tropic_certificates: [DEVICE_CERT_TROPIC, CA_CERT_TROPIC], + tropic_signature: SIGNATURE_TROPIC, + }, + challenge: Buffer.from(CHALLENGE, 'hex'), + deviceModel: 'T3W1', + config: { + version: 1, + T3W1: { + rootPubKeysOptiga: [], + rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC], + rootPubKeysMLDSA: [], + }, + }, + }); + + expect(result.verified).toBe(false); + expect(result.error).toBe('RESPONSE_PAYLOAD_MISSING'); + }); +}); + +describe('ML-DSA-44 verification', () => { + it('verifies the exact message signature and rejects a different challenge', () => { + const keys = ml_dsa44.keygen(); + const message = Uint8Array.from([1, 2, 3, 4]); + const signature = ml_dsa44.sign(message, keys.secretKey); + + expect(verifySignatureMLDSA44(keys.publicKey, message, signature)).toBe(true); + expect(verifySignatureMLDSA44(keys.publicKey, Uint8Array.from([1, 2, 3, 5]), signature)).toBe( + false + ); + }); }); describe('M1: CA public key blacklist (revocation)', () => { diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts index a8b73ddfe..e9e642038 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts @@ -1,4 +1,5 @@ import type { DeviceAuthenticityConfig } from './types'; +import { T3W1_ROOT_PUB_KEYS_MLDSA } from './trezorRootKeysMLDSA'; /** * Trezor manufacturing root CA public keys, per internal model. @@ -6,9 +7,6 @@ import type { DeviceAuthenticityConfig } from './types'; * https://github.com/trezor/trezor-firmware/blob/main/core/embed/projects/prodtest/cmd/hsm_keys.h * Mirrored from @trezor/device-authenticity `deviceAuthenticityConfig`. * - * Note: ML-DSA (rootPubKeysMLDSA) keys are intentionally omitted — ML-DSA - * verification is not supported here (see verifySignatures.ts), and the Optiga - * P-256 path already provides a valid per-device identity for every model. * Devices without a secure element (T1B1, T2T1) do not support AuthenticateDevice * and are therefore absent from this config. */ @@ -63,6 +61,7 @@ export const deviceAuthenticityConfig: DeviceAuthenticityConfig = { // upstream: DEV_AUTH_ROOT_PROD_BACKUP_ED25519 '5612606584ee7e0bc313b13f7ac94156bb4cb75bd77585ddbe579301306e85f1', ], + rootPubKeysMLDSA: [...T3W1_ROOT_PUB_KEYS_MLDSA], debug: { rootPubKeysOptiga: [ // upstream: DEV_AUTH_ROOT_DEBUG_P256 diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts index 7d849334f..80a1c2025 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts @@ -25,9 +25,9 @@ export type AuthenticateDeviceResult = { * the explicit model policy verified up to a trusted Trezor root CA AND the * device signed our challenge: * - Optiga (all secure-element models), and - * - Tropic + matching serial number (T3W1). - * ML-DSA / MCU is NOT checked (needs @noble/post-quantum). The reward backend - * must independently verify raw proof under its own versioned policy. + * - Tropic + MCU/ML-DSA + matching serial numbers (T3W1). + * The reward backend must independently verify the same raw proof under its + * own versioned policy; this client result is only a UX preview. */ verified: boolean; /** @@ -65,11 +65,15 @@ const modelConfigOf = (config: DeviceAuthenticityConfig, deviceModel: string) => export const getRequiredDeviceAuthenticityLayers = ( deviceModel: string -): readonly ('optiga' | 'tropic')[] => (deviceModel === 'T3W1' ? ['optiga', 'tropic'] : ['optiga']); +): readonly ('optiga' | 'tropic' | 'mcu')[] => + deviceModel === 'T3W1' ? ['optiga', 'tropic', 'mcu'] : ['optiga']; const isTropicExpected = (deviceModel: string): boolean => getRequiredDeviceAuthenticityLayers(deviceModel).includes('tropic'); +const isMcuExpected = (deviceModel: string): boolean => + getRequiredDeviceAuthenticityLayers(deviceModel).includes('mcu'); + const matchedDebugKey = ( config: DeviceAuthenticityConfig, deviceModel: string, @@ -78,9 +82,11 @@ const matchedDebugKey = ( if (!rootPubKey) return false; const m = modelConfigOf(config, deviceModel); if (!m?.debug) return false; - return [...(m.debug.rootPubKeysOptiga ?? []), ...(m.debug.rootPubKeysTropic ?? [])].includes( - rootPubKey - ); + return [ + ...(m.debug.rootPubKeysOptiga ?? []), + ...(m.debug.rootPubKeysTropic ?? []), + ...(m.debug.rootPubKeysMLDSA ?? []), + ].includes(rootPubKey); }; /** @@ -126,10 +132,10 @@ export const authenticateDeviceFromProof = ({ }; } - // 2) On T3W1 the Tropic layer is also required, and its serial number must - // match Optiga's. This defeats a transplanted-Optiga forgery: an attacker - // moving a genuine Optiga onto a fake board cannot also satisfy Tropic. + // 2) On T3W1 the independent Tropic and MCU layers are also required. + // Their certificate serial numbers must match Optiga's. let usedDebugKey = matchedDebugKey(config, deviceModel, optiga.rootPubKey); + const serialNumbers = [optiga.serialNumber]; if (isTropicExpected(deviceModel)) { const { tropic_certificates: tropicCertificates, tropic_signature: tropicSignature } = proof; if (!tropicSignature || !tropicCertificates?.length) { @@ -143,12 +149,32 @@ export const authenticateDeviceFromProof = ({ if (!tropic.valid) { return { verified: false, error: tropic.error }; } - const serialsPresent = !!optiga.serialNumber && !!tropic.serialNumber; - if (!serialsPresent || optiga.serialNumber !== tropic.serialNumber) { - return { verified: false, error: 'SERIAL_NUMBER_MISMATCH' }; - } + serialNumbers.push(tropic.serialNumber); usedDebugKey = usedDebugKey || matchedDebugKey(config, deviceModel, tropic.rootPubKey); } + if (isMcuExpected(deviceModel)) { + const { mcu_certificates: mcuCertificates, mcu_signature: mcuSignature } = proof; + if (!mcuSignature || !mcuCertificates?.length) { + return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; + } + const mcu = verifyAuthenticityProof({ + ...common, + certificates: mcuCertificates, + signature: mcuSignature, + }); + if (!mcu.valid) { + return { verified: false, error: mcu.error }; + } + serialNumbers.push(mcu.serialNumber); + usedDebugKey = usedDebugKey || matchedDebugKey(config, deviceModel, mcu.rootPubKey); + } + if ( + serialNumbers.length > 1 && + (serialNumbers.some(serialNumber => !serialNumber) || + serialNumbers.some(serialNumber => serialNumber !== serialNumbers[0])) + ) { + return { verified: false, error: 'SERIAL_NUMBER_MISMATCH' }; + } const deviceId = optiga.serialNumber ?? diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts new file mode 100644 index 000000000..defaa75ac --- /dev/null +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts @@ -0,0 +1,6 @@ +// Synchronized from Trezor's public @trezor/device-authenticity config. +// These are public verification keys; manufacturing private keys never leave Trezor. +export const T3W1_ROOT_PUB_KEYS_MLDSA = [ + 'ea67d6000e79d16bc9119cea88b3415369d612f02a6bca6e18e0dbe5e2d37da716b993a05258a642c656a4e1c80f2db1f2818e5ebcbe5715448389079526895f8e620001b1c1dc857aa6165ec1d0b731127c424cbf0dec1ae33bbce500b9e7974d2a83626fe0be91555d79e1f91fbcb5df821738385cba29dc1a71c3ef2d1d3b3165ff66b4638b7121db9ecdfeb39d20163732d1685f7604956de9f549c5a7fcbf2a27b913a94107904cdbfe486df4a62dc27f0f7ba14c1103f9de86480c12bcaf1bab70584277a9fdac50d03d50e9e7bb6dd3b8b61cafe07c5513c4fbb35d14766fef0582ca1d78ebc4fb246ee3ac231eef770e74ad4207dddc8f3666700c84aa1446cb5ceef0b6f113041a9eeb8708564a4c43fdb46203dd1a2b2c48a0e485bc10a72a8a8e5b006d3d398941b33365b04b54d14d230950c7df048afd2c16ff1515387a97b712f68f65f08fd722d4b75c97007c4f7dfff5e5f22908b56bf40acd5263cc419007a355bcd1f4a3dd201286e58be1165e0eedf6c236c7aaa7cff3a8fbd6aee5d3ea5368bbf0aad8f41147ce874e2c97ed3fdfe859def143f63c1e7a093ce18615016a857e875a4099d7a56b036c7bdba205ff21841c4693ecfe08ae8faf10c73fe3a8e202a585c39ae8b084f2879a2c20faf8abe5163eb8acfffde4b9a73a9706bb5d3ae91235f673df8f20f5dd72c06df8a7401924c614eb12d915deffacba689738612780877f39a891b3cc0673c728653f72874386d6a097516d56c196c9f1515a62b5ddbab2c795d48be8b6a304533034f323c6970d43a08957d8b0db7cf80e6c02589069f9bf622e60f6953bfd10d68cac3d421bf9145169fa0b10d2384212556e042d95686c44bacd01b8b2e698163ca6d2b965557e8e16f0d3672bb32c0dc3bbced78f75918df59742fcb9834925b50c568ec305db53e3d7915851620f0763820ad511896041d7a836b764b51273f6ab1e4021e67bcc5095c02f55c132b674d47b36271830508c6d08e7578181fcf95c9027c43bebc56b0d083d6bbde10e70113ae41102181f147bd6aad58898d296b14ec4f7e049ff47831949b8f5aa29a2c74e7ea26a4270e84d4ace36407b255767f9eb9676d15559d1c403b1a5949a9ca2a74e6e0f2719b11306911799be9dea597ec8b375bac646f8a9a03e6f2271d9fb5b695c51d7fe2125a08e243d63e5d426b6dbbc149a87e65f8b1888465f99ce519ce4dd8d51432e45e4bd01a41893189a8656bc9c9226fc4bbc38441569d3be87180c833d63d53adc15d953ec6fcd1643aa00f20aea7cc169efeeb962ebb724ba0008861a1fda988c93d3ea0e3224d4c72c0c2fecf4f954f9fcf47b5f39b59a9c4c12423382ae2ef547310490bb3d57661a51b9ee356d0720f6e376e2374c38e738e158dbec8d7cc5a23a6269a3ca441a4ec406ddeef5a0d6007d0b3590ba19eeb2a6da67fe21b68625d1d422739c928c7b9a49163a6ffe60cfce4cfa918b690fe5b300022e88ff164020ce9815d18c97a63ad3383e1101eb523c5e1d20482dd6d0411b13b75c89e864fa6bede2674bfa5c3283a1487a3093ff05dba3f86420a7a9ac62c59971fa9d10c9ebc68f36d65e798f95c3a2cb11993ae1454855cda4d8290f2f0aa8f48cbddad92211a1e1a38588822159c257c077a7bd1c1afeb3644e6001a1f0dc0833993c068879f484d6a4da8c1fb85959d282be52583225b60c31cf1ebe0e74bbd3fc74caf039ad3c03f59b5da694fa810e199ad7587b593548c828a18c6a9b657df6cb39e839f36beffc07c6e85c8bbd7e98646af879bead4d3e1b79c401eb6449a4200f1cbdad39329fc43c5553a286a0', + 'fedf8b0c16164b9cec427882aa03d3bd6a1588cfbaebaf93cf2fc52f275417777433a938dcd6516345148f728c935a40ef3b6c41fd3ed831fea13774bd39887da8b06e2e671cc32a1c72e44a13d1cd985dc2ddcb63e457bbd29d38186ddb776b8e73b645c8bf616309f890d175538a9d10c82e595dea03f70564ff69b53497ecefd51fd8320041d9b1092386033ed37a93dd9b4b9da1297f808eac21020b8dc4747e01854ebdc407a04fc18b2758720125acb41f0c60e16cd53707a65392feed06c9fcc28a3e738dbc91614e89e5d535274c6b226d83f335a0dd088755571cf62496ca4642ec8e65eba8be2b8cec46416c6a3ad4be10569ac532e02d6e06b6ce0c0d4b7a99a11c910f9f0631b0cd72e20637237eeff723e722f7aa626732434c47843a099739e96e31267d8d6905d8b2b712a5a69d9343a3c1e07afb414b73950f4b1df196243005fb61c6200ef346826d6c0f4cc8ede052ed48235997bb366dade4483bfc11fe65fd0c5539e89d8bd271e54e30da6e5192a0f343dea3806aa798d12d62251a063d071ba67c35d3a26e582c2d361cf2d0fc36fb1b5a46e469669338b1c1744bdfe365700a6a1c641ab2b7906e2b7c0c0a9af8862ab5751a678e3e0156847ff6093ca7ebd48dd8b1d6f7736c2039e705c1e20f42ed5cac6db3dc7a7e2facbb75411e7fb8a156df52a2926afa9070937cde4dd4bd456294ba59bdcf123f11dcda267d475721abfecab1b31848a27236aa751e5b55015f033d87fb9cf5c086e09f1667cf7ec2e72436c907c5763690d698fffa997709bb89526cae772bb962c06ed1b89d418835672b85e1a1b2e3c55f4e607ecad09bdb3a18224b9805659aa2ecf0b62dbf6265e27d4101450edb590d52d525b62b1114dad2eb06c951a2c77c7ecfbf5f1f6fe285f39727958ce0b5cfe8fd465420c270309999e6694ee31fcb7d6480a75d0d72558cd6a050b8d11a9a6d7736700027fbe14b48949b7e1af2ca148ad4e4b4b93e9575a4119afd1b57c250606d5a4e258c2393c104d651995b1e75887f492ff05e92e1630332f176d86a0453b412d0869d98d6ab27377a74766e3cadde29c1853e6398a9edfa48c0d14e7c148b048f1bf25d11979e94878cb86eaf1e55638c1b39e0ac5254a009bf957f4b624796753c3451246e5a7b9022926aa77b7a1b4ae7d25f776d4395ba98a37d9e21b221da29564e50a669056650a40489386e8909c6ca21d369c3495c44b2f71373a392c9fa89c9085f467d24d6219fcdd2b56126792eb6a14269c4264a56926b1a7d9a1c03d9649d157f938abfdc2a56c20795065774838030da41a4c6178919cc1c6c88de1458d9baa1e51a4a1994e6d5cc94b049b15c235c5cf46d2cf23f09d565c5a5497ef0b3a5fbc8d1be8ed78f3794c49d4936139b0419faaffc52b55cb91f6e1bafacd55523c0b7eec56d98e4228cd0f9d00bc9866dac71e43db2b2cf311999d7bc3a49219836f558cbb5d8faa79e83079c6ce92288aae852553b655ef6bc87fbb25ca3ab482aecaf0a86eecc92341906cc89d234087f5abd6d2100a60d361d516d460d436b46706ad44b8b4ef3fe54ea198ffc2cf8c8e144ec62c4ce86ed5d181283fc17c926668ca4e0b8683e0c7bc662a54ea7f4c830365ad1cdea58abee9a726168ce9870729972884ecfe5baf06f1ee664aa422e26f9f76e20908d72e78c1a2dcd94fe1c07573d7706d7bdc07d0b41498c0d52ec3a90d6e7c0fcef597a884fb2caba29c2279569a1e64dd6c7ed4b312d2197b6391407899b25d21cefd1408eca42204f76cb0436f44c0a9c2e1eca19df8d93255c0da6469dc47da883e186387f76011cc8', +] as const; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts index 57e38298d..dda557dd1 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts @@ -8,20 +8,20 @@ import { validateCaCertExtensions } from './validateCaCertExtensions'; import { getVerifyFn } from './verifySignatures'; import { type ParsedCertificate, parseCertificate } from './x509certificate'; -const getChunkSize = (byteLength: number): Buffer => { - if (byteLength < 0xfd) return Buffer.from([byteLength]); +const getChunkSize = (byteLength: number): Uint8Array => { + if (byteLength < 0xfd) return Uint8Array.from([byteLength]); if (byteLength <= 0xffff) { const b = Buffer.alloc(3); b[0] = 0xfd; b.writeUInt16LE(byteLength, 1); - return b; + return Uint8Array.from(b); } const b = Buffer.alloc(5); b[0] = 0xfe; b.writeUInt32LE(byteLength, 1); - return b; + return Uint8Array.from(b); }; // Compose the data against which the signatures are verified: each chunk is @@ -33,9 +33,18 @@ export const prepareDeviceAuthenticityData = ({ payload: Buffer | Buffer[]; prefix?: string; }): Uint8Array => { - const chunks = [Buffer.from(prefix), ...(Array.isArray(payload) ? payload : [payload])]; - - return Buffer.concat(chunks.flatMap(chunk => [getChunkSize(chunk.byteLength), chunk])); + const chunks = [ + Uint8Array.from(Buffer.from(prefix)), + ...(Array.isArray(payload) ? payload : [payload]).map(chunk => Uint8Array.from(chunk)), + ]; + const framed = chunks.flatMap(chunk => [getChunkSize(chunk.byteLength), chunk]); + const result = new Uint8Array(framed.reduce((total, chunk) => total + chunk.byteLength, 0)); + let offset = 0; + for (const chunk of framed) { + result.set(chunk, offset); + offset += chunk.byteLength; + } + return result; }; const getRootPubKeys = ({ @@ -54,6 +63,7 @@ const getRootPubKeys = ({ const prod = [ ...(modelConfig.rootPubKeysOptiga ?? []), ...(modelConfig.rootPubKeysTropic ?? []), + ...(modelConfig.rootPubKeysMLDSA ?? []), ]; if (!allowDebugKeys) return prod; @@ -61,6 +71,7 @@ const getRootPubKeys = ({ ...prod, ...(modelConfig.debug?.rootPubKeysOptiga ?? []), ...(modelConfig.debug?.rootPubKeysTropic ?? []), + ...(modelConfig.debug?.rootPubKeysMLDSA ?? []), ]; }; @@ -75,10 +86,10 @@ const matchRootPubKeyToCertificate = ({ return allRootPubKeys.find(rootPubKey => verifySignatureFn( - Buffer.from(rootPubKey, 'hex'), + Uint8Array.from(Buffer.from(rootPubKey, 'hex')), cert.tbsCertificate.asn1.raw, - cert.signatureValue.bits.bytes, - ), + cert.signatureValue.bits.bytes + ) ); }; @@ -115,25 +126,63 @@ export const verifyAuthenticityProof = ({ const allRootPubKeys = getRootPubKeys({ config, deviceModel, allowDebugKeys }); const parsedCertificates = certificates.map(c => - parseCertificate(new Uint8Array(Buffer.from(c, 'hex'))), + parseCertificate(new Uint8Array(Buffer.from(c, 'hex'))) ); const firstCertAlgName = parsedCertificates[0]?.signatureAlgorithm.algorithmName; - // Only the [device, CA] scheme (P-256 / Ed25519) is supported. The single-cert - // ML-DSA scheme (T3W1 secondary) is not verifiable here. - if (firstCertAlgName !== 'P-256' && firstCertAlgName !== 'Ed25519') { + if (firstCertAlgName === 'MLDSA44') { + if (parsedCertificates.length !== 1) { + return { valid: false, error: 'RESPONSE_MALFORMED' }; + } + const [deviceCert] = parsedCertificates; + const deviceCertPubKey = Buffer.from( + deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes + ).toString('hex'); + const rootPubKeyMatch = matchRootPubKeyToCertificate({ + allRootPubKeys, + cert: deviceCert, + }); + if (rootPubKeyMatch === undefined) { + return { valid: false, deviceCertPubKey, error: 'ROOT_PUBKEY_NOT_FOUND' }; + } + if (parseModelFromDeviceCertSubject(deviceCert) !== deviceModel) { + return { + valid: false, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'INVALID_DEVICE_MODEL', + }; + } + const isSignatureValid = getVerifyFn('MLDSA44')( + Uint8Array.from(deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes), + signedData, + Uint8Array.from(Buffer.from(signature, 'hex')) + ); + if (!isSignatureValid) { + return { + valid: false, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + error: 'INVALID_DEVICE_SIGNATURE', + }; + } return { - valid: false, - error: firstCertAlgName === 'MLDSA44' ? 'UNSUPPORTED_ALGORITHM' : 'RESPONSE_MALFORMED', + valid: true, + rootPubKey: rootPubKeyMatch, + deviceCertPubKey, + serialNumber: parseSerialNumberFromDeviceCert(deviceCert), }; } + if (firstCertAlgName !== 'P-256' && firstCertAlgName !== 'Ed25519') { + return { valid: false, error: 'RESPONSE_MALFORMED' }; + } if (parsedCertificates.length !== 2) { return { valid: false, error: 'RESPONSE_MALFORMED' }; } const [deviceCert, caCert] = parsedCertificates; const deviceCertPubKey = Buffer.from( - deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes, + deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes ).toString('hex'); const deviceCertAlgName = deviceCert.signatureAlgorithm.algorithmName; @@ -163,6 +212,10 @@ export const verifyAuthenticityProof = ({ error: 'CA_PUBKEY_BLACKLISTED', }; } + const caCertValidityFrom = caCert.tbsCertificate.validity.from.getTime(); + if (caCertValidityFrom > Date.now()) { + throw new Error(`CA validity from ${caCertValidityFrom} can't be in the future`); + } // 2) Device model in the certificate must match the connected device. const modelFromSubject = parseModelFromDeviceCertSubject(deviceCert); @@ -178,9 +231,9 @@ export const verifyAuthenticityProof = ({ // 3) Device certificate must be signed by the CA pub key. const isDeviceCertValid = verifySignatureFn( - Buffer.from(caPubKeyBytes), + Uint8Array.from(caPubKeyBytes), deviceCert.tbsCertificate.asn1.raw, - deviceCert.signatureValue.bits.bytes, + deviceCert.signatureValue.bits.bytes ); if (!isDeviceCertValid) { return { @@ -194,9 +247,9 @@ export const verifyAuthenticityProof = ({ // 4) The challenge signature must be produced by the device key. const isSignatureValid = verifySignatureFn( - Buffer.from(deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes), + Uint8Array.from(deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes), signedData, - Buffer.from(signature, 'hex'), + Uint8Array.from(Buffer.from(signature, 'hex')) ); if (!isSignatureValid) { return { diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts index afa7ccd33..85e16e34e 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts @@ -1,13 +1,14 @@ import { ed25519 } from '@noble/curves/ed25519'; import { p256 } from '@noble/curves/p256'; import { sha256 } from '@noble/hashes/sha256'; +import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'; import { type AlgorithmName, fixSignature } from './x509certificate'; export type VerifySignature = ( rawKey: Uint8Array, data: Uint8Array, - signature: Uint8Array, + signature: Uint8Array ) => boolean; // P-256 (secp256r1) ECDSA over SHA-256. Hardware attestation signatures are NOT @@ -33,12 +34,17 @@ export const verifySignatureEd25519: VerifySignature = (rawKey, data, signature) } }; +export const verifySignatureMLDSA44: VerifySignature = (rawKey, data, signature) => { + try { + return ml_dsa44.verify(signature, data, rawKey); + } catch { + return false; + } +}; + export const getVerifyFn = (algorithmName: AlgorithmName): VerifySignature => { if (algorithmName === 'P-256') return verifySignatureP256; if (algorithmName === 'Ed25519') return verifySignatureEd25519; - // MLDSA44 is a T3W1-only secondary (Tropic/MCU) signature and needs - // @noble/post-quantum, which is not a dependency here. The primary Optiga - // (P-256) result already yields a valid per-device identity for every - // attestation-capable model, so ML-DSA verification is intentionally unsupported. + if (algorithmName === 'MLDSA44') return verifySignatureMLDSA44; throw new Error(`Unsupported signature algorithm: ${algorithmName}`); }; diff --git a/packages/hwk-trezor-adapter/tsup.config.ts b/packages/hwk-trezor-adapter/tsup.config.ts index b812e199e..7188afd3d 100644 --- a/packages/hwk-trezor-adapter/tsup.config.ts +++ b/packages/hwk-trezor-adapter/tsup.config.ts @@ -15,6 +15,10 @@ export default defineConfig({ clean: true, sourcemap: true, splitting: true, + // Local debug-watch copies only this package's dist into app-monorepo. + // Bundle the ML-DSA implementation so that flow works without publishing or + // separately installing a new transitive package in every client platform. + noExternal: [/^@noble\/post-quantum/, /^@noble\/ciphers/, /^@noble\/curves/, /^@noble\/hashes/], external: [ '@onekeyfe/hwk-adapter-core', '@onekeyfe/hwk-trezor-protobuf', diff --git a/packages/hwk-trezor-connector/src/__tests__/TrezorConnectorBase.test.ts b/packages/hwk-trezor-connector/src/__tests__/TrezorConnectorBase.test.ts index f8e91f603..35519bceb 100644 --- a/packages/hwk-trezor-connector/src/__tests__/TrezorConnectorBase.test.ts +++ b/packages/hwk-trezor-connector/src/__tests__/TrezorConnectorBase.test.ts @@ -303,6 +303,143 @@ describe('TrezorConnectorBase device settings', () => { }); }); +describe('TrezorConnectorBase device authenticity streaming', () => { + it('keeps the legacy single-response proof as a firmware fallback', async () => { + const proof = { + optiga_certificates: ['aabb'], + optiga_signature: 'ccdd', + tropic_certificates: [], + mcu_certificates: [], + }; + const connector = new SessionBackedTestTrezorConnector( + [{ connectId: 'safe-3', deviceId: 'safe-3', name: 'Trezor Safe 3', model: 'T2B1' }], + trezorFeatures({ internal_model: 'T2B1' }), + [{ type: 'AuthenticityProof', message: proof }] + ); + + const session = await connector.connect('safe-3'); + await expect( + callConnector(connector, session.sessionId, 'authenticateDevice', { + challenge: 'ab'.repeat(32), + }) + ).resolves.toEqual(proof); + expect(connector.fakeSessions[0].calls).toEqual([ + { + name: 'AuthenticateDevice', + data: { challenge: 'ab'.repeat(32), stream: true }, + }, + ]); + }); + + it('assembles every Optiga, Tropic, and MCU proof part from streamed chunks', async () => { + const connector = new SessionBackedTestTrezorConnector( + [{ connectId: 'safe-7', deviceId: 'safe-7', name: 'Trezor Safe 7', model: 'T3W1' }], + trezorFeatures({ internal_model: 'T3W1' }), + [ + { + type: 'AuthenticityProofSizes', + message: { + optiga_certificates: [2], + optiga_signature: 2, + tropic_certificates: [2], + tropic_signature: 2, + mcu_certificates: [2], + mcu_signature: 2, + }, + }, + { type: 'AuthenticityProofChunk', message: { chunk: 'aabb' } }, + { type: 'AuthenticityProofChunk', message: { chunk: 'ccdd' } }, + { type: 'AuthenticityProofChunk', message: { chunk: '1122' } }, + { type: 'AuthenticityProofChunk', message: { chunk: '3344' } }, + { type: 'AuthenticityProofChunk', message: { chunk: '5566' } }, + { type: 'AuthenticityProofChunk', message: { chunk: '7788' } }, + { type: 'Success', message: { message: 'Success' } }, + ] + ); + + const session = await connector.connect('safe-7'); + await expect( + callConnector(connector, session.sessionId, 'authenticateDevice', { + challenge: 'ab'.repeat(32), + }) + ).resolves.toEqual({ + optiga_certificates: ['aabb'], + optiga_signature: 'ccdd', + tropic_certificates: ['1122'], + tropic_signature: '3344', + mcu_certificates: ['5566'], + mcu_signature: '7788', + }); + expect(connector.fakeSessions[0].calls).toEqual([ + { + name: 'AuthenticateDevice', + data: { challenge: 'ab'.repeat(32), stream: true }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 0, index: 0, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 0, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 1, index: 0, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 1, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 2, index: 0, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { proof_type: 2, offset: 0, size: 2 }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { offset: 0, size: 0 }, + }, + ]); + }); + + it('rejects an oversized certificate chain before requesting proof chunks', async () => { + const connector = new SessionBackedTestTrezorConnector( + [{ connectId: 'safe-7', deviceId: 'safe-7', name: 'Trezor Safe 7', model: 'T3W1' }], + trezorFeatures({ internal_model: 'T3W1' }), + [ + { + type: 'AuthenticityProofSizes', + message: { + optiga_certificates: [1, 1, 1, 1, 1], + }, + }, + { type: 'Success', message: { message: 'Success' } }, + ] + ); + + const session = await connector.connect('safe-7'); + await expect( + callConnector(connector, session.sessionId, 'authenticateDevice', { + challenge: 'ab'.repeat(32), + }) + ).rejects.toThrow('too many certificates'); + expect(connector.fakeSessions[0].calls).toEqual([ + { + name: 'AuthenticateDevice', + data: { challenge: 'ab'.repeat(32), stream: true }, + }, + { + name: 'GetAuthenticityProofChunk', + data: { offset: 0, size: 0 }, + }, + ]); + }); +}); + const tronCapableFeatures = () => trezorFeatures({ capabilities: ['Capability_Tron'] }); const numericTronCapableFeatures = () => trezorFeatures({ capabilities: [24] }); diff --git a/packages/hwk-trezor-connector/src/index.ts b/packages/hwk-trezor-connector/src/index.ts index 964eda576..2160ba1c0 100644 --- a/packages/hwk-trezor-connector/src/index.ts +++ b/packages/hwk-trezor-connector/src/index.ts @@ -133,6 +133,143 @@ function getTrezorResponseMessage(response: unknown): Record { return {}; } +const AUTHENTICITY_PROOF_CHUNK_SIZE = 500; +const MAX_AUTHENTICITY_PROOF_PART_SIZE = 64 * 1024; +const MAX_AUTHENTICITY_CERTIFICATE_COUNT = 4; + +type AuthenticityProofSizes = { + optiga_certificates?: number[]; + optiga_signature?: number; + tropic_certificates?: number[]; + tropic_signature?: number; + mcu_certificates?: number[]; + mcu_signature?: number; +}; + +const validateAuthenticityProofPartSize = (size: unknown): number => { + if ( + typeof size !== 'number' || + !Number.isSafeInteger(size) || + size < 0 || + size > MAX_AUTHENTICITY_PROOF_PART_SIZE + ) { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: `Invalid Trezor authenticity proof part size: ${String(size)}`, + }); + } + return size; +}; + +const readAuthenticityProofPart = async ({ + deviceSession, + proofType, + index, + size, +}: { + deviceSession: TrezorDeviceSession; + proofType: number; + index?: number; + size: number; +}): Promise => { + const expectedSize = validateAuthenticityProofPartSize(size); + let offset = 0; + let result = ''; + while (offset < expectedSize) { + const requestedSize = Math.min(AUTHENTICITY_PROOF_CHUNK_SIZE, expectedSize - offset); + const request: Record = { + proof_type: proofType, + offset, + size: requestedSize, + }; + if (index !== undefined) request.index = index; + const response = (await deviceSession.call( + 'GetAuthenticityProofChunk', + request + )) as TrezorMessageResponse; + if (response.type !== 'AuthenticityProofChunk') { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: `Expected AuthenticityProofChunk, received ${response.type}`, + }); + } + const chunk = response.message?.chunk; + if (typeof chunk !== 'string' || chunk.length !== requestedSize * 2) { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: 'Trezor returned an invalid authenticity proof chunk', + }); + } + result += chunk; + offset += requestedSize; + } + return result; +}; + +const readAuthenticityProofStream = async ( + deviceSession: TrezorDeviceSession, + sizes: AuthenticityProofSizes +): Promise> => { + const readCertificates = async (proofType: number, values: unknown): Promise => { + if (!Array.isArray(values)) { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: 'Trezor returned invalid authenticity certificate sizes', + }); + } + if (values.length > MAX_AUTHENTICITY_CERTIFICATE_COUNT) { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: `Trezor authenticity proof contains too many certificates: ${values.length}`, + }); + } + const certificates: string[] = []; + for (let index = 0; index < values.length; index += 1) { + certificates.push( + await readAuthenticityProofPart({ + deviceSession, + proofType, + index, + size: validateAuthenticityProofPartSize(values[index]), + }) + ); + } + return certificates; + }; + const readSignature = async (proofType: number, size: unknown): Promise => { + if (size === undefined || size === null || size === 0) return undefined; + return readAuthenticityProofPart({ + deviceSession, + proofType, + size: validateAuthenticityProofPartSize(size), + }); + }; + + try { + // Sequential reads are required: the firmware stream permits one outstanding + // chunk request at a time. + const optigaCertificates = await readCertificates(0, sizes.optiga_certificates ?? []); + const optigaSignature = await readSignature(0, sizes.optiga_signature); + const tropicCertificates = await readCertificates(1, sizes.tropic_certificates ?? []); + const tropicSignature = await readSignature(1, sizes.tropic_signature); + const mcuCertificates = await readCertificates(2, sizes.mcu_certificates ?? []); + const mcuSignature = await readSignature(2, sizes.mcu_signature); + + return { + optiga_certificates: optigaCertificates, + optiga_signature: optigaSignature, + tropic_certificates: tropicCertificates, + tropic_signature: tropicSignature, + mcu_certificates: mcuCertificates, + mcu_signature: mcuSignature, + }; + } finally { + // Firmware keeps the proof stream open until this empty request. Always + // release it, including malformed/host-aborted responses. + await deviceSession.call('GetAuthenticityProofChunk', { offset: 0, size: 0 }); + } +}; + export abstract class TrezorConnectorBase implements IConnector { readonly connectionType: ConnectionType; @@ -438,12 +575,25 @@ export abstract class TrezorConnectorBase implements IConnector { case 'wipeDevice': return getTrezorResponseMessage(await session.deviceSession.call('WipeDevice', {})); case 'authenticateDevice': { - // Device attestation: the device signs the host challenge with its - // secure-element (Optiga) key and returns its certificate chain. Requires - // an on-device confirmation and a secure-element model (Safe 3/5/T3W1). + // Current Safe 7 firmware streams the larger Optiga + Tropic + MCU/ML-DSA + // proof. Older secure-element models return AuthenticityProof directly. const p = (params ?? {}) as { challenge: string }; - return getTrezorResponseMessage( - await session.deviceSession.call('AuthenticateDevice', { challenge: p.challenge }) + const response = (await session.deviceSession.call('AuthenticateDevice', { + challenge: p.challenge, + stream: true, + })) as TrezorMessageResponse; + if (response.type === 'AuthenticityProof') { + return getTrezorResponseMessage(response); + } + if (response.type !== 'AuthenticityProofSizes') { + throw createHwkError({ + code: HardwareErrorCode.UnknownError, + message: `Expected Trezor authenticity proof, received ${response.type}`, + }); + } + return readAuthenticityProofStream( + session.deviceSession, + response.message as AuthenticityProofSizes ); } // --- THP application-session control (Trezor-specific, driven by the diff --git a/packages/hwk-trezor-protobuf/src/definitions/index.ts b/packages/hwk-trezor-protobuf/src/definitions/index.ts index f0d3888ef..057d4cf16 100644 --- a/packages/hwk-trezor-protobuf/src/definitions/index.ts +++ b/packages/hwk-trezor-protobuf/src/definitions/index.ts @@ -143,6 +143,8 @@ import { ApplySettings, AuthenticateDevice, AuthenticityProof, + AuthenticityProofChunk, + AuthenticityProofSizes, BackupDevice, Cancel, CancelAuthorization, @@ -161,6 +163,7 @@ import { Features, FirmwareHash, GetEntropy, + GetAuthenticityProofChunk, GetFeatures, GetFirmwareHash, GetNextU2FCounter, @@ -426,6 +429,9 @@ export const MessageType = Type.Object( FirmwareHash, AuthenticateDevice, AuthenticityProof, + AuthenticityProofSizes, + GetAuthenticityProofChunk, + AuthenticityProofChunk, WipeDevice, LoadDevice, ResetDevice, @@ -670,7 +676,8 @@ export type WireInMessage = | 'Cancel' | 'GetEntropy' | 'GetFirmwareHash' - | 'AuthenticityProof' + | 'AuthenticateDevice' + | 'GetAuthenticityProofChunk' | 'WipeDevice' | 'LoadDevice' | 'ResetDevice' @@ -788,7 +795,9 @@ export type WireOutMessage = | 'DataChunkRequest' | 'Entropy' | 'FirmwareHash' - | 'AuthenticateDevice' + | 'AuthenticityProof' + | 'AuthenticityProofSizes' + | 'AuthenticityProofChunk' | 'EntropyRequest' | 'EntropyCheckReady' | 'WordRequest' diff --git a/packages/hwk-trezor-protobuf/src/definitions/messages-management.ts b/packages/hwk-trezor-protobuf/src/definitions/messages-management.ts index 97aef1567..62b6bd8b4 100644 --- a/packages/hwk-trezor-protobuf/src/definitions/messages-management.ts +++ b/packages/hwk-trezor-protobuf/src/definitions/messages-management.ts @@ -218,6 +218,7 @@ export type AuthenticateDevice = Static; export const AuthenticateDevice = Type.Object( { challenge: Type.String(), + stream: Type.Optional(Type.Boolean()), }, { $id: 'AuthenticateDevice' }, ); @@ -235,6 +236,47 @@ export const AuthenticityProof = Type.Object( { $id: 'AuthenticityProof' }, ); +export type AuthenticityProofSizes = Static; +export const AuthenticityProofSizes = Type.Object( + { + optiga_certificates: Type.Array(Type.Number()), + optiga_signature: Type.Number(), + tropic_certificates: Type.Array(Type.Number()), + tropic_signature: Type.Optional(Type.Number()), + mcu_certificates: Type.Array(Type.Number()), + mcu_signature: Type.Optional(Type.Number()), + }, + { $id: 'AuthenticityProofSizes' }, +); + +export enum AuthenticityProofType { + OPTIGA = 0, + TROPIC = 1, + MCU = 2, +} + +export type EnumAuthenticityProofType = Static; +export const EnumAuthenticityProofType = Type.Enum(AuthenticityProofType); + +export type GetAuthenticityProofChunk = Static; +export const GetAuthenticityProofChunk = Type.Object( + { + proof_type: Type.Optional(EnumAuthenticityProofType), + index: Type.Optional(Type.Number()), + offset: Type.Number(), + size: Type.Number(), + }, + { $id: 'GetAuthenticityProofChunk' }, +); + +export type AuthenticityProofChunk = Static; +export const AuthenticityProofChunk = Type.Object( + { + chunk: Type.String(), + }, + { $id: 'AuthenticityProofChunk' }, +); + export type Slip39Group = Static; export const Slip39Group = Type.Object( { diff --git a/packages/hwk-trezor-protobuf/src/definitions/messages-management_pb.js b/packages/hwk-trezor-protobuf/src/definitions/messages-management_pb.js index ca2a63ec8..e4d4c74c4 100644 --- a/packages/hwk-trezor-protobuf/src/definitions/messages-management_pb.js +++ b/packages/hwk-trezor-protobuf/src/definitions/messages-management_pb.js @@ -1,91 +1,580 @@ // @generated by protoc-gen-es v2.11.0 with parameter "target=js,json_types=true" // @generated from file messages-management.proto (package hw.trezor.messages.management, syntax proto2) -import { enumDesc, fileDesc, messageDesc, tsEnum } from '@bufbuild/protobuf/codegenv2'; - -import { file_options } from './options_pb'; -export const file_messages_management = fileDesc( - 'ChltZXNzYWdlcy1tYW5hZ2VtZW50LnByb3RvEh1ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudCJWCgpJbml0aWFsaXplEhIKCnNlc3Npb25faWQYASABKAwSHAoQX3NraXBfcGFzc3BocmFzZRgCIAEoCEICGAESFgoOZGVyaXZlX2NhcmRhbm8YAyABKAgiDQoLR2V0RmVhdHVyZXMiuxQKCEZlYXR1cmVzEg4KBnZlbmRvchgBIAEoCRIVCg1tYWpvcl92ZXJzaW9uGAIgAigNEhUKDW1pbm9yX3ZlcnNpb24YAyACKA0SFQoNcGF0Y2hfdmVyc2lvbhgEIAIoDRIVCg1idWlsZF92ZXJzaW9uGD0gASgNEhcKD2Jvb3Rsb2FkZXJfbW9kZRgFIAEoCBIRCglkZXZpY2VfaWQYBiABKAkSFgoOcGluX3Byb3RlY3Rpb24YByABKAgSHQoVcGFzc3BocmFzZV9wcm90ZWN0aW9uGAggASgIEhAKCGxhbmd1YWdlGAkgASgJEg0KBWxhYmVsGAogASgJEhMKC2luaXRpYWxpemVkGAwgASgIEhAKCHJldmlzaW9uGA0gASgMEhcKD2Jvb3Rsb2FkZXJfaGFzaBgOIAEoDBIQCghpbXBvcnRlZBgPIAEoCBIQCgh1bmxvY2tlZBgQIAEoCBIeChJfcGFzc3BocmFzZV9jYWNoZWQYESABKAhCAhgBEhgKEGZpcm13YXJlX3ByZXNlbnQYEiABKAgSVwoTYmFja3VwX2F2YWlsYWJpbGl0eRgTIAEoDjI6Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLkJhY2t1cEF2YWlsYWJpbGl0eRINCgVmbGFncxgUIAEoDRINCgVtb2RlbBgVIAEoCRIQCghmd19tYWpvchgWIAEoDRIQCghmd19taW5vchgXIAEoDRIQCghmd19wYXRjaBgYIAEoDRIQCghmd19idWlsZBg+IAEoDRIRCglmd192ZW5kb3IYGSABKAkSGQoRdW5maW5pc2hlZF9iYWNrdXAYGyABKAgSEQoJbm9fYmFja3VwGBwgASgIEk8KD3JlY292ZXJ5X3N0YXR1cxgdIAEoDjI2Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLlJlY292ZXJ5U3RhdHVzEkgKDGNhcGFiaWxpdGllcxgeIAMoDjIyLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLkNhcGFiaWxpdHkSPgoLYmFja3VwX3R5cGUYHyABKA4yKS5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5CYWNrdXBUeXBlEhcKD3NkX2NhcmRfcHJlc2VudBggIAEoCBIVCg1zZF9wcm90ZWN0aW9uGCEgASgIEhwKFHdpcGVfY29kZV9wcm90ZWN0aW9uGCIgASgIEhIKCnNlc3Npb25faWQYIyABKAwSIwobcGFzc3BocmFzZV9hbHdheXNfb25fZGV2aWNlGCQgASgIEkYKDXNhZmV0eV9jaGVja3MYJSABKA4yLy5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5TYWZldHlDaGVja0xldmVsEhoKEmF1dG9fbG9ja19kZWxheV9tcxgmIAEoDRJIChBkaXNwbGF5X3JvdGF0aW9uGCcgASgOMi4uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuRGlzcGxheVJvdGF0aW9uEh0KFWV4cGVyaW1lbnRhbF9mZWF0dXJlcxgoIAEoCBIMCgRidXN5GCkgASgIEkoKEWhvbWVzY3JlZW5fZm9ybWF0GCogASgOMi8uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuSG9tZXNjcmVlbkZvcm1hdBIhChloaWRlX3Bhc3NwaHJhc2VfZnJvbV9ob3N0GCsgASgIEhYKDmludGVybmFsX21vZGVsGCwgASgJEhIKCnVuaXRfY29sb3IYLSABKA0SFAoMdW5pdF9idGNvbmx5GC4gASgIEhgKEGhvbWVzY3JlZW5fd2lkdGgYLyABKA0SGQoRaG9tZXNjcmVlbl9oZWlnaHQYMCABKA0SGQoRYm9vdGxvYWRlcl9sb2NrZWQYMSABKAgSJgoYbGFuZ3VhZ2VfdmVyc2lvbl9tYXRjaGVzGDIgASgIOgR0cnVlEhYKDnVuaXRfcGFja2FnaW5nGDMgASgNEhcKD2hhcHRpY19mZWVkYmFjaxg0IAEoCBJCCg1yZWNvdmVyeV90eXBlGDUgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuUmVjb3ZlcnlUeXBlEhIKCm9wdGlnYV9zZWMYNiABKA0SCwoDc29jGDcgASgNEhoKEmZpcm13YXJlX2NvcnJ1cHRlZBg4IAEoCBIiChphdXRvX2xvY2tfZGVsYXlfYmF0dGVyeV9tcxg5IAEoDRILCgNsZWQYOiABKAgSFQoNdXNiX2Nvbm5lY3RlZBg7IAEoCBIaChJ3aXJlbGVzc19jb25uZWN0ZWQYPCABKAgiQwoSQmFja3VwQXZhaWxhYmlsaXR5EhAKDE5vdEF2YWlsYWJsZRAAEgwKCFJlcXVpcmVkEAESDQoJQXZhaWxhYmxlEAIiNwoOUmVjb3ZlcnlTdGF0dXMSCwoHTm90aGluZxAAEgwKCFJlY292ZXJ5EAESCgoGQmFja3VwEAIipgUKCkNhcGFiaWxpdHkSHAoSQ2FwYWJpbGl0eV9CaXRjb2luEAEaBICmHQESGwoXQ2FwYWJpbGl0eV9CaXRjb2luX2xpa2UQAhIWChJDYXBhYmlsaXR5X0JpbmFuY2UQAxIWChJDYXBhYmlsaXR5X0NhcmRhbm8QBBIbChFDYXBhYmlsaXR5X0NyeXB0bxAFGgSAph0BEhIKDkNhcGFiaWxpdHlfRU9TEAYSFwoTQ2FwYWJpbGl0eV9FdGhlcmV1bRAHEhcKD0NhcGFiaWxpdHlfTGlzaxAIGgIIARIVChFDYXBhYmlsaXR5X01vbmVybxAJEhIKDkNhcGFiaWxpdHlfTkVNEAoSFQoRQ2FwYWJpbGl0eV9SaXBwbGUQCxIWChJDYXBhYmlsaXR5X1N0ZWxsYXIQDBIUChBDYXBhYmlsaXR5X1Rlem9zEA0SEgoOQ2FwYWJpbGl0eV9VMkYQDhIbChFDYXBhYmlsaXR5X1NoYW1pchAPGgSAph0BEiEKF0NhcGFiaWxpdHlfU2hhbWlyR3JvdXBzEBAaBICmHQESJAoaQ2FwYWJpbGl0eV9QYXNzcGhyYXNlRW50cnkQERoEgKYdARIVChFDYXBhYmlsaXR5X1NvbGFuYRASEiEKF0NhcGFiaWxpdHlfVHJhbnNsYXRpb25zEBMaBICmHQESHwoVQ2FwYWJpbGl0eV9CcmlnaHRuZXNzEBQaBICmHQESGwoRQ2FwYWJpbGl0eV9IYXB0aWMQFRoEgKYdARIYCg5DYXBhYmlsaXR5X0JMRRAWGgSAph0BEhgKDkNhcGFiaWxpdHlfTkZDEBcaBICmHQESEwoPQ2FwYWJpbGl0eV9Ucm9uEBgSGQoPQ2FwYWJpbGl0eV9ONFcxEBkaBICmHQEaBMjzGAEiDAoKTG9ja0RldmljZSIcCgdTZXRCdXN5EhEKCWV4cGlyeV9tcxgBIAEoDSIMCgpFbmRTZXNzaW9uIu0DCg1BcHBseVNldHRpbmdzEhQKCGxhbmd1YWdlGAEgASgJQgIYARINCgVsYWJlbBgCIAEoCRIWCg51c2VfcGFzc3BocmFzZRgDIAEoCBISCgpob21lc2NyZWVuGAQgASgMEh4KEl9wYXNzcGhyYXNlX3NvdXJjZRgFIAEoDUICGAESGgoSYXV0b19sb2NrX2RlbGF5X21zGAYgASgNEkgKEGRpc3BsYXlfcm90YXRpb24YByABKA4yLi5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5EaXNwbGF5Um90YXRpb24SIwobcGFzc3BocmFzZV9hbHdheXNfb25fZGV2aWNlGAggASgIEkYKDXNhZmV0eV9jaGVja3MYCSABKA4yLy5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5TYWZldHlDaGVja0xldmVsEh0KFWV4cGVyaW1lbnRhbF9mZWF0dXJlcxgKIAEoCBIhChloaWRlX3Bhc3NwaHJhc2VfZnJvbV9ob3N0GAsgASgIEhcKD2hhcHRpY19mZWVkYmFjaxgNIAEoCBIZChFob21lc2NyZWVuX2xlbmd0aBgOIAEoDRIiChphdXRvX2xvY2tfZGVsYXlfYmF0dGVyeV9tcxgPIAEoDSI7Cg5DaGFuZ2VMYW5ndWFnZRITCgtkYXRhX2xlbmd0aBgBIAIoDRIUCgxzaG93X2Rpc3BsYXkYAiABKAgiPAoQRGF0YUNodW5rUmVxdWVzdBITCgtkYXRhX2xlbmd0aBgBIAIoDRITCgtkYXRhX29mZnNldBgCIAIoDSIiCgxEYXRhQ2h1bmtBY2sSEgoKZGF0YV9jaHVuaxgBIAIoDCIbCgpBcHBseUZsYWdzEg0KBWZsYWdzGAEgAigNIhsKCUNoYW5nZVBpbhIOCgZyZW1vdmUYASABKAgiIAoOQ2hhbmdlV2lwZUNvZGUSDgoGcmVtb3ZlGAEgASgIIp8BCglTZFByb3RlY3QSUgoJb3BlcmF0aW9uGAEgAigOMj8uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuU2RQcm90ZWN0LlNkUHJvdGVjdE9wZXJhdGlvblR5cGUiPgoWU2RQcm90ZWN0T3BlcmF0aW9uVHlwZRILCgdESVNBQkxFEAASCgoGRU5BQkxFEAESCwoHUkVGUkVTSBACIjQKBFBpbmcSEQoHbWVzc2FnZRgBIAEoCToAEhkKEWJ1dHRvbl9wcm90ZWN0aW9uGAIgASgIIggKBkNhbmNlbCIaCgpHZXRFbnRyb3B5EgwKBHNpemUYASACKA0iGgoHRW50cm9weRIPCgdlbnRyb3B5GAEgAigMIiQKD0dldEZpcm13YXJlSGFzaBIRCgljaGFsbGVuZ2UYASABKAwiHAoMRmlybXdhcmVIYXNoEgwKBGhhc2gYASACKAwiJwoSQXV0aGVudGljYXRlRGV2aWNlEhEKCWNoYWxsZW5nZRgBIAIoDCKyAQoRQXV0aGVudGljaXR5UHJvb2YSGwoTb3B0aWdhX2NlcnRpZmljYXRlcxgBIAMoDBIYChBvcHRpZ2Ffc2lnbmF0dXJlGAIgAigMEhsKE3Ryb3BpY19jZXJ0aWZpY2F0ZXMYAyADKAwSGAoQdHJvcGljX3NpZ25hdHVyZRgEIAEoDBIYChBtY3VfY2VydGlmaWNhdGVzGAUgAygMEhUKDW1jdV9zaWduYXR1cmUYBiABKAwiDAoKV2lwZURldmljZSLgAQoKTG9hZERldmljZRIRCgltbmVtb25pY3MYASADKAkSCwoDcGluGAMgASgJEh0KFXBhc3NwaHJhc2VfcHJvdGVjdGlvbhgEIAEoCBIUCghsYW5ndWFnZRgFIAEoCUICGAESDQoFbGFiZWwYBiABKAkSFQoNc2tpcF9jaGVja3N1bRgHIAEoCBITCgt1MmZfY291bnRlchgIIAEoDRIUCgxuZWVkc19iYWNrdXAYCSABKAgSEQoJbm9fYmFja3VwGAogASgIEhkKEXVuZmluaXNoZWRfYmFja3VwGAsgASgIIu4CCgtSZXNldERldmljZRIVCghzdHJlbmd0aBgCIAEoDToDMjU2Eh0KFXBhc3NwaHJhc2VfcHJvdGVjdGlvbhgDIAEoCBIWCg5waW5fcHJvdGVjdGlvbhgEIAEoCBIUCghsYW5ndWFnZRgFIAEoCUICGAESDQoFbGFiZWwYBiABKAkSEwoLdTJmX2NvdW50ZXIYByABKA0SEwoLc2tpcF9iYWNrdXAYCCABKAgSEQoJbm9fYmFja3VwGAkgASgIEkUKC2JhY2t1cF90eXBlGAogASgOMikuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwVHlwZToFQmlwMzkSFQoNZW50cm9weV9jaGVjaxgLIAEoCBJLCg1iYWNrdXBfbWV0aG9kGAwgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwTWV0aG9kOgdEaXNwbGF5SgQIARACIvwBCgxCYWNrdXBEZXZpY2USFwoPZ3JvdXBfdGhyZXNob2xkGAEgASgNEkcKBmdyb3VwcxgCIAMoCzI3Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkJhY2t1cERldmljZS5TbGlwMzlHcm91cBJLCg1iYWNrdXBfbWV0aG9kGAMgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwTWV0aG9kOgdEaXNwbGF5Gj0KC1NsaXAzOUdyb3VwEhgKEG1lbWJlcl90aHJlc2hvbGQYASACKA0SFAoMbWVtYmVyX2NvdW50GAIgAigNIkIKDkVudHJvcHlSZXF1ZXN0EhoKEmVudHJvcHlfY29tbWl0bWVudBgBIAEoDBIUCgxwcmV2X2VudHJvcHkYAiABKAwiHQoKRW50cm9weUFjaxIPCgdlbnRyb3B5GAEgAigMIhMKEUVudHJvcHlDaGVja1JlYWR5Ii0KFEVudHJvcHlDaGVja0NvbnRpbnVlEhUKBmZpbmlzaBgBIAEoCDoFZmFsc2Ui4AMKDlJlY292ZXJ5RGV2aWNlEhIKCndvcmRfY291bnQYASABKA0SHQoVcGFzc3BocmFzZV9wcm90ZWN0aW9uGAIgASgIEhYKDnBpbl9wcm90ZWN0aW9uGAMgASgIEhQKCGxhbmd1YWdlGAQgASgJQgIYARINCgVsYWJlbBgFIAEoCRIYChBlbmZvcmNlX3dvcmRsaXN0GAYgASgIEl0KDGlucHV0X21ldGhvZBgIIAEoDjJHLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlY292ZXJ5RGV2aWNlLlJlY292ZXJ5RGV2aWNlSW5wdXRNZXRob2QSEwoLdTJmX2NvdW50ZXIYCSABKA0SSQoEdHlwZRgKIAEoDjIrLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlY292ZXJ5VHlwZToOTm9ybWFsUmVjb3ZlcnkSQgoNYmFja3VwX21ldGhvZBgLIAEoDjIrLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkJhY2t1cE1ldGhvZCI7ChlSZWNvdmVyeURldmljZUlucHV0TWV0aG9kEhIKDlNjcmFtYmxlZFdvcmRzEAASCgoGTWF0cml4EAFKBAgHEAgivwEKC1dvcmRSZXF1ZXN0EkgKBHR5cGUYASACKA4yOi5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5Xb3JkUmVxdWVzdC5Xb3JkUmVxdWVzdFR5cGUiZgoPV29yZFJlcXVlc3RUeXBlEhkKFVdvcmRSZXF1ZXN0VHlwZV9QbGFpbhAAEhsKF1dvcmRSZXF1ZXN0VHlwZV9NYXRyaXg5EAESGwoXV29yZFJlcXVlc3RUeXBlX01hdHJpeDYQAiIXCgdXb3JkQWNrEgwKBHdvcmQYASACKAkiJAoNU2V0VTJGQ291bnRlchITCgt1MmZfY291bnRlchgBIAIoDSITChFHZXROZXh0VTJGQ291bnRlciIlCg5OZXh0VTJGQ291bnRlchITCgt1MmZfY291bnRlchgBIAIoDSIRCg9Eb1ByZWF1dGhvcml6ZWQiFgoUUHJlYXV0aG9yaXplZFJlcXVlc3QiFQoTQ2FuY2VsQXV0aG9yaXphdGlvbiLOAQoSUmVib290VG9Cb290bG9hZGVyEmIKDGJvb3RfY29tbWFuZBgBIAEoDjI9Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlYm9vdFRvQm9vdGxvYWRlci5Cb290Q29tbWFuZDoNU1RPUF9BTkRfV0FJVBIXCg9maXJtd2FyZV9oZWFkZXIYAiABKAwiNQoLQm9vdENvbW1hbmQSEQoNU1RPUF9BTkRfV0FJVBAAEhMKD0lOU1RBTExfVVBHUkFERRABSgQIAxAEIgoKCEdldE5vbmNlIhYKBU5vbmNlEg0KBW5vbmNlGAEgAigMIiwKClVubG9ja1BhdGgSEQoJYWRkcmVzc19uGAEgAygNEgsKA21hYxgCIAEoDCIiChNVbmxvY2tlZFBhdGhSZXF1ZXN0EgsKA21hYxgBIAIoDCIUChJTaG93RGV2aWNlVHV0b3JpYWwiEgoQVW5sb2NrQm9vdGxvYWRlciIeCg1TZXRCcmlnaHRuZXNzEg0KBXZhbHVlGAEgASgNIhEKD0dldFNlcmlhbE51bWJlciIlCgxTZXJpYWxOdW1iZXISFQoNc2VyaWFsX251bWJlchgBIAIoCSqZAQoKQmFja3VwVHlwZRIJCgVCaXAzORAAEhAKDFNsaXAzOV9CYXNpYxABEhMKD1NsaXAzOV9BZHZhbmNlZBACEhwKGFNsaXAzOV9TaW5nbGVfRXh0ZW5kYWJsZRADEhsKF1NsaXAzOV9CYXNpY19FeHRlbmRhYmxlEAQSHgoaU2xpcDM5X0FkdmFuY2VkX0V4dGVuZGFibGUQBSolCgxCYWNrdXBNZXRob2QSCwoHRGlzcGxheRAAEggKBE40VzEQASpHChBTYWZldHlDaGVja0xldmVsEgoKBlN0cmljdBAAEhAKDFByb21wdEFsd2F5cxABEhUKEVByb21wdFRlbXBvcmFyaWx5EAIqPQoPRGlzcGxheVJvdGF0aW9uEgkKBU5vcnRoEAASCAoERWFzdBBaEgoKBVNvdXRoELQBEgkKBFdlc3QQjgIqMAoQSG9tZXNjcmVlbkZvcm1hdBIICgRUb2lmEAESCAoESnBlZxACEggKBFRvaUcQAypICgxSZWNvdmVyeVR5cGUSEgoOTm9ybWFsUmVjb3ZlcnkQABIKCgZEcnlSdW4QARIYChRVbmxvY2tSZXBlYXRlZEJhY2t1cBACQkIKI2NvbS5zYXRvc2hpbGFicy50cmV6b3IubGliLnByb3RvYnVmQhdUcmV6b3JNZXNzYWdlTWFuYWdlbWVudICmHQE', - [file_options], -); -export const InitializeSchema = messageDesc(file_messages_management, 0); -export const GetFeaturesSchema = messageDesc(file_messages_management, 1); -export const FeaturesSchema = messageDesc(file_messages_management, 2); -export const Features_BackupAvailabilitySchema = enumDesc(file_messages_management, 2, 0); -export const Features_BackupAvailability = tsEnum(Features_BackupAvailabilitySchema); -export const Features_RecoveryStatusSchema = enumDesc(file_messages_management, 2, 1); -export const Features_RecoveryStatus = tsEnum(Features_RecoveryStatusSchema); -export const Features_CapabilitySchema = enumDesc(file_messages_management, 2, 2); -export const Features_Capability = tsEnum(Features_CapabilitySchema); -export const LockDeviceSchema = messageDesc(file_messages_management, 3); -export const SetBusySchema = messageDesc(file_messages_management, 4); -export const EndSessionSchema = messageDesc(file_messages_management, 5); -export const ApplySettingsSchema = messageDesc(file_messages_management, 6); -export const ChangeLanguageSchema = messageDesc(file_messages_management, 7); -export const DataChunkRequestSchema = messageDesc(file_messages_management, 8); -export const DataChunkAckSchema = messageDesc(file_messages_management, 9); -export const ApplyFlagsSchema = messageDesc(file_messages_management, 10); -export const ChangePinSchema = messageDesc(file_messages_management, 11); -export const ChangeWipeCodeSchema = messageDesc(file_messages_management, 12); -export const SdProtectSchema = messageDesc(file_messages_management, 13); -export const SdProtect_SdProtectOperationTypeSchema = enumDesc(file_messages_management, 13, 0); -export const SdProtect_SdProtectOperationType = tsEnum(SdProtect_SdProtectOperationTypeSchema); -export const PingSchema = messageDesc(file_messages_management, 14); -export const CancelSchema = messageDesc(file_messages_management, 15); -export const GetEntropySchema = messageDesc(file_messages_management, 16); -export const EntropySchema = messageDesc(file_messages_management, 17); -export const GetFirmwareHashSchema = messageDesc(file_messages_management, 18); -export const FirmwareHashSchema = messageDesc(file_messages_management, 19); -export const AuthenticateDeviceSchema = messageDesc(file_messages_management, 20); -export const AuthenticityProofSchema = messageDesc(file_messages_management, 21); -export const WipeDeviceSchema = messageDesc(file_messages_management, 22); -export const LoadDeviceSchema = messageDesc(file_messages_management, 23); -export const ResetDeviceSchema = messageDesc(file_messages_management, 24); -export const BackupDeviceSchema = messageDesc(file_messages_management, 25); -export const BackupDevice_Slip39GroupSchema = messageDesc(file_messages_management, 25, 0); -export const EntropyRequestSchema = messageDesc(file_messages_management, 26); -export const EntropyAckSchema = messageDesc(file_messages_management, 27); -export const EntropyCheckReadySchema = messageDesc(file_messages_management, 28); -export const EntropyCheckContinueSchema = messageDesc(file_messages_management, 29); -export const RecoveryDeviceSchema = messageDesc(file_messages_management, 30); -export const RecoveryDevice_RecoveryDeviceInputMethodSchema = enumDesc( - file_messages_management, - 30, - 0, -); -export const RecoveryDevice_RecoveryDeviceInputMethod = tsEnum( - RecoveryDevice_RecoveryDeviceInputMethodSchema, -); -export const WordRequestSchema = messageDesc(file_messages_management, 31); -export const WordRequest_WordRequestTypeSchema = enumDesc(file_messages_management, 31, 0); -export const WordRequest_WordRequestType = tsEnum(WordRequest_WordRequestTypeSchema); -export const WordAckSchema = messageDesc(file_messages_management, 32); -export const SetU2FCounterSchema = messageDesc(file_messages_management, 33); -export const GetNextU2FCounterSchema = messageDesc(file_messages_management, 34); -export const NextU2FCounterSchema = messageDesc(file_messages_management, 35); -export const DoPreauthorizedSchema = messageDesc(file_messages_management, 36); -export const PreauthorizedRequestSchema = messageDesc(file_messages_management, 37); -export const CancelAuthorizationSchema = messageDesc(file_messages_management, 38); -export const RebootToBootloaderSchema = messageDesc(file_messages_management, 39); -export const RebootToBootloader_BootCommandSchema = enumDesc(file_messages_management, 39, 0); -export const RebootToBootloader_BootCommand = tsEnum(RebootToBootloader_BootCommandSchema); -export const GetNonceSchema = messageDesc(file_messages_management, 40); -export const NonceSchema = messageDesc(file_messages_management, 41); -export const UnlockPathSchema = messageDesc(file_messages_management, 42); -export const UnlockedPathRequestSchema = messageDesc(file_messages_management, 43); -export const ShowDeviceTutorialSchema = messageDesc(file_messages_management, 44); -export const UnlockBootloaderSchema = messageDesc(file_messages_management, 45); -export const SetBrightnessSchema = messageDesc(file_messages_management, 46); -export const GetSerialNumberSchema = messageDesc(file_messages_management, 47); -export const SerialNumberSchema = messageDesc(file_messages_management, 48); -export const BackupTypeSchema = enumDesc(file_messages_management, 0); -export const BackupType = tsEnum(BackupTypeSchema); -export const BackupMethodSchema = enumDesc(file_messages_management, 1); -export const BackupMethod = tsEnum(BackupMethodSchema); -export const SafetyCheckLevelSchema = enumDesc(file_messages_management, 2); -export const SafetyCheckLevel = tsEnum(SafetyCheckLevelSchema); -export const DisplayRotationSchema = enumDesc(file_messages_management, 3); -export const DisplayRotation = tsEnum(DisplayRotationSchema); -export const HomescreenFormatSchema = enumDesc(file_messages_management, 4); -export const HomescreenFormat = tsEnum(HomescreenFormatSchema); -export const RecoveryTypeSchema = enumDesc(file_messages_management, 5); -export const RecoveryType = tsEnum(RecoveryTypeSchema); +/* eslint-disable */ + +import { enumDesc, fileDesc, messageDesc, tsEnum } from "@bufbuild/protobuf/codegenv2"; +import { file_options } from "./options_pb"; + +/** + * Describes the file messages-management.proto. + */ +export const file_messages_management = /*@__PURE__*/ + fileDesc("ChltZXNzYWdlcy1tYW5hZ2VtZW50LnByb3RvEh1ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudCJWCgpJbml0aWFsaXplEhIKCnNlc3Npb25faWQYASABKAwSHAoQX3NraXBfcGFzc3BocmFzZRgCIAEoCEICGAESFgoOZGVyaXZlX2NhcmRhbm8YAyABKAgiDQoLR2V0RmVhdHVyZXMi+BQKCEZlYXR1cmVzEg4KBnZlbmRvchgBIAEoCRIVCg1tYWpvcl92ZXJzaW9uGAIgAigNEhUKDW1pbm9yX3ZlcnNpb24YAyACKA0SFQoNcGF0Y2hfdmVyc2lvbhgEIAIoDRIVCg1idWlsZF92ZXJzaW9uGD0gASgNEhcKD2Jvb3Rsb2FkZXJfbW9kZRgFIAEoCBIRCglkZXZpY2VfaWQYBiABKAkSFgoOcGluX3Byb3RlY3Rpb24YByABKAgSHQoVcGFzc3BocmFzZV9wcm90ZWN0aW9uGAggASgIEhAKCGxhbmd1YWdlGAkgASgJEg0KBWxhYmVsGAogASgJEhMKC2luaXRpYWxpemVkGAwgASgIEhAKCHJldmlzaW9uGA0gASgMEhcKD2Jvb3Rsb2FkZXJfaGFzaBgOIAEoDBIQCghpbXBvcnRlZBgPIAEoCBIQCgh1bmxvY2tlZBgQIAEoCBIeChJfcGFzc3BocmFzZV9jYWNoZWQYESABKAhCAhgBEhgKEGZpcm13YXJlX3ByZXNlbnQYEiABKAgSVwoTYmFja3VwX2F2YWlsYWJpbGl0eRgTIAEoDjI6Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLkJhY2t1cEF2YWlsYWJpbGl0eRINCgVmbGFncxgUIAEoDRINCgVtb2RlbBgVIAEoCRIQCghmd19tYWpvchgWIAEoDRIQCghmd19taW5vchgXIAEoDRIQCghmd19wYXRjaBgYIAEoDRIQCghmd19idWlsZBg+IAEoDRIRCglmd192ZW5kb3IYGSABKAkSGQoRdW5maW5pc2hlZF9iYWNrdXAYGyABKAgSEQoJbm9fYmFja3VwGBwgASgIEk8KD3JlY292ZXJ5X3N0YXR1cxgdIAEoDjI2Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLlJlY292ZXJ5U3RhdHVzEkgKDGNhcGFiaWxpdGllcxgeIAMoDjIyLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkZlYXR1cmVzLkNhcGFiaWxpdHkSPgoLYmFja3VwX3R5cGUYHyABKA4yKS5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5CYWNrdXBUeXBlEhcKD3NkX2NhcmRfcHJlc2VudBggIAEoCBIVCg1zZF9wcm90ZWN0aW9uGCEgASgIEhwKFHdpcGVfY29kZV9wcm90ZWN0aW9uGCIgASgIEhIKCnNlc3Npb25faWQYIyABKAwSIwobcGFzc3BocmFzZV9hbHdheXNfb25fZGV2aWNlGCQgASgIEkYKDXNhZmV0eV9jaGVja3MYJSABKA4yLy5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5TYWZldHlDaGVja0xldmVsEhoKEmF1dG9fbG9ja19kZWxheV9tcxgmIAEoDRJIChBkaXNwbGF5X3JvdGF0aW9uGCcgASgOMi4uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuRGlzcGxheVJvdGF0aW9uEh0KFWV4cGVyaW1lbnRhbF9mZWF0dXJlcxgoIAEoCBIMCgRidXN5GCkgASgIEkoKEWhvbWVzY3JlZW5fZm9ybWF0GCogASgOMi8uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuSG9tZXNjcmVlbkZvcm1hdBIhChloaWRlX3Bhc3NwaHJhc2VfZnJvbV9ob3N0GCsgASgIEhYKDmludGVybmFsX21vZGVsGCwgASgJEhIKCnVuaXRfY29sb3IYLSABKA0SFAoMdW5pdF9idGNvbmx5GC4gASgIEhgKEGhvbWVzY3JlZW5fd2lkdGgYLyABKA0SGQoRaG9tZXNjcmVlbl9oZWlnaHQYMCABKA0SGQoRYm9vdGxvYWRlcl9sb2NrZWQYMSABKAgSJgoYbGFuZ3VhZ2VfdmVyc2lvbl9tYXRjaGVzGDIgASgIOgR0cnVlEhYKDnVuaXRfcGFja2FnaW5nGDMgASgNEhcKD2hhcHRpY19mZWVkYmFjaxg0IAEoCBJCCg1yZWNvdmVyeV90eXBlGDUgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuUmVjb3ZlcnlUeXBlEhIKCm9wdGlnYV9zZWMYNiABKA0SCwoDc29jGDcgASgNEhoKEmZpcm13YXJlX2NvcnJ1cHRlZBg4IAEoCBIiChphdXRvX2xvY2tfZGVsYXlfYmF0dGVyeV9tcxg5IAEoDRILCgNsZWQYOiABKAgSFQoNdXNiX2Nvbm5lY3RlZBg7IAEoCBIaChJ3aXJlbGVzc19jb25uZWN0ZWQYPCABKAgSEwoLdGFwX3RvX3dha2UYPyABKAgiQwoSQmFja3VwQXZhaWxhYmlsaXR5EhAKDE5vdEF2YWlsYWJsZRAAEgwKCFJlcXVpcmVkEAESDQoJQXZhaWxhYmxlEAIiNwoOUmVjb3ZlcnlTdGF0dXMSCwoHTm90aGluZxAAEgwKCFJlY292ZXJ5EAESCgoGQmFja3VwEAIizgUKCkNhcGFiaWxpdHkSHAoSQ2FwYWJpbGl0eV9CaXRjb2luEAEaBICmHQESGwoXQ2FwYWJpbGl0eV9CaXRjb2luX2xpa2UQAhIWChJDYXBhYmlsaXR5X0JpbmFuY2UQAxIWChJDYXBhYmlsaXR5X0NhcmRhbm8QBBIbChFDYXBhYmlsaXR5X0NyeXB0bxAFGgSAph0BEhIKDkNhcGFiaWxpdHlfRU9TEAYSFwoTQ2FwYWJpbGl0eV9FdGhlcmV1bRAHEhcKD0NhcGFiaWxpdHlfTGlzaxAIGgIIARIVChFDYXBhYmlsaXR5X01vbmVybxAJEhIKDkNhcGFiaWxpdHlfTkVNEAoSFQoRQ2FwYWJpbGl0eV9SaXBwbGUQCxIWChJDYXBhYmlsaXR5X1N0ZWxsYXIQDBIUChBDYXBhYmlsaXR5X1Rlem9zEA0SEgoOQ2FwYWJpbGl0eV9VMkYQDhIbChFDYXBhYmlsaXR5X1NoYW1pchAPGgSAph0BEiEKF0NhcGFiaWxpdHlfU2hhbWlyR3JvdXBzEBAaBICmHQESJAoaQ2FwYWJpbGl0eV9QYXNzcGhyYXNlRW50cnkQERoEgKYdARIVChFDYXBhYmlsaXR5X1NvbGFuYRASEiEKF0NhcGFiaWxpdHlfVHJhbnNsYXRpb25zEBMaBICmHQESHwoVQ2FwYWJpbGl0eV9CcmlnaHRuZXNzEBQaBICmHQESGwoRQ2FwYWJpbGl0eV9IYXB0aWMQFRoEgKYdARIYCg5DYXBhYmlsaXR5X0JMRRAWGgSAph0BEhgKDkNhcGFiaWxpdHlfTkZDEBcaBICmHQESEwoPQ2FwYWJpbGl0eV9Ucm9uEBgSGQoPQ2FwYWJpbGl0eV9ONFcxEBkaBICmHQESIAoWQ2FwYWJpbGl0eV9Ub3VjaFdha2V1cBAaGgSAph0BGgTI8xgBIgQIGxAbIgwKCkxvY2tEZXZpY2UiHAoHU2V0QnVzeRIRCglleHBpcnlfbXMYASABKA0iDAoKRW5kU2Vzc2lvbiLtAwoNQXBwbHlTZXR0aW5ncxIUCghsYW5ndWFnZRgBIAEoCUICGAESDQoFbGFiZWwYAiABKAkSFgoOdXNlX3Bhc3NwaHJhc2UYAyABKAgSEgoKaG9tZXNjcmVlbhgEIAEoDBIeChJfcGFzc3BocmFzZV9zb3VyY2UYBSABKA1CAhgBEhoKEmF1dG9fbG9ja19kZWxheV9tcxgGIAEoDRJIChBkaXNwbGF5X3JvdGF0aW9uGAcgASgOMi4uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuRGlzcGxheVJvdGF0aW9uEiMKG3Bhc3NwaHJhc2VfYWx3YXlzX29uX2RldmljZRgIIAEoCBJGCg1zYWZldHlfY2hlY2tzGAkgASgOMi8uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuU2FmZXR5Q2hlY2tMZXZlbBIdChVleHBlcmltZW50YWxfZmVhdHVyZXMYCiABKAgSIQoZaGlkZV9wYXNzcGhyYXNlX2Zyb21faG9zdBgLIAEoCBIXCg9oYXB0aWNfZmVlZGJhY2sYDSABKAgSGQoRaG9tZXNjcmVlbl9sZW5ndGgYDiABKA0SIgoaYXV0b19sb2NrX2RlbGF5X2JhdHRlcnlfbXMYDyABKA0iOwoOQ2hhbmdlTGFuZ3VhZ2USEwoLZGF0YV9sZW5ndGgYASACKA0SFAoMc2hvd19kaXNwbGF5GAIgASgIIjwKEERhdGFDaHVua1JlcXVlc3QSEwoLZGF0YV9sZW5ndGgYASACKA0SEwoLZGF0YV9vZmZzZXQYAiACKA0iIgoMRGF0YUNodW5rQWNrEhIKCmRhdGFfY2h1bmsYASACKAwiGwoKQXBwbHlGbGFncxINCgVmbGFncxgBIAIoDSIbCglDaGFuZ2VQaW4SDgoGcmVtb3ZlGAEgASgIIiAKDkNoYW5nZVdpcGVDb2RlEg4KBnJlbW92ZRgBIAEoCCKfAQoJU2RQcm90ZWN0ElIKCW9wZXJhdGlvbhgBIAIoDjI/Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlNkUHJvdGVjdC5TZFByb3RlY3RPcGVyYXRpb25UeXBlIj4KFlNkUHJvdGVjdE9wZXJhdGlvblR5cGUSCwoHRElTQUJMRRAAEgoKBkVOQUJMRRABEgsKB1JFRlJFU0gQAiI0CgRQaW5nEhEKB21lc3NhZ2UYASABKAk6ABIZChFidXR0b25fcHJvdGVjdGlvbhgCIAEoCCIICgZDYW5jZWwiGgoKR2V0RW50cm9weRIMCgRzaXplGAEgAigNIhoKB0VudHJvcHkSDwoHZW50cm9weRgBIAIoDCIkCg9HZXRGaXJtd2FyZUhhc2gSEQoJY2hhbGxlbmdlGAEgASgMIhwKDEZpcm13YXJlSGFzaBIMCgRoYXNoGAEgAigMIjcKEkF1dGhlbnRpY2F0ZURldmljZRIRCgljaGFsbGVuZ2UYASACKAwSDgoGc3RyZWFtGAIgASgIIrIBChFBdXRoZW50aWNpdHlQcm9vZhIbChNvcHRpZ2FfY2VydGlmaWNhdGVzGAEgAygMEhgKEG9wdGlnYV9zaWduYXR1cmUYAiACKAwSGwoTdHJvcGljX2NlcnRpZmljYXRlcxgDIAMoDBIYChB0cm9waWNfc2lnbmF0dXJlGAQgASgMEhgKEG1jdV9jZXJ0aWZpY2F0ZXMYBSADKAwSFQoNbWN1X3NpZ25hdHVyZRgGIAEoDCK3AQoWQXV0aGVudGljaXR5UHJvb2ZTaXplcxIbChNvcHRpZ2FfY2VydGlmaWNhdGVzGAEgAygNEhgKEG9wdGlnYV9zaWduYXR1cmUYAiACKA0SGwoTdHJvcGljX2NlcnRpZmljYXRlcxgDIAMoDRIYChB0cm9waWNfc2lnbmF0dXJlGAQgASgNEhgKEG1jdV9jZXJ0aWZpY2F0ZXMYBSADKA0SFQoNbWN1X3NpZ25hdHVyZRgGIAEoDSLmAQoZR2V0QXV0aGVudGljaXR5UHJvb2ZDaHVuaxJiCgpwcm9vZl90eXBlGAEgASgOMk4uaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuR2V0QXV0aGVudGljaXR5UHJvb2ZDaHVuay5BdXRoZW50aWNpdHlQcm9vZlR5cGUSDQoFaW5kZXgYAiABKA0SDgoGb2Zmc2V0GAMgAigNEgwKBHNpemUYBCACKA0iOAoVQXV0aGVudGljaXR5UHJvb2ZUeXBlEgoKBk9QVElHQRAAEgoKBlRST1BJQxABEgcKA01DVRACIicKFkF1dGhlbnRpY2l0eVByb29mQ2h1bmsSDQoFY2h1bmsYASACKAwiDAoKV2lwZURldmljZSLgAQoKTG9hZERldmljZRIRCgltbmVtb25pY3MYASADKAkSCwoDcGluGAMgASgJEh0KFXBhc3NwaHJhc2VfcHJvdGVjdGlvbhgEIAEoCBIUCghsYW5ndWFnZRgFIAEoCUICGAESDQoFbGFiZWwYBiABKAkSFQoNc2tpcF9jaGVja3N1bRgHIAEoCBITCgt1MmZfY291bnRlchgIIAEoDRIUCgxuZWVkc19iYWNrdXAYCSABKAgSEQoJbm9fYmFja3VwGAogASgIEhkKEXVuZmluaXNoZWRfYmFja3VwGAsgASgIIu4CCgtSZXNldERldmljZRIVCghzdHJlbmd0aBgCIAEoDToDMjU2Eh0KFXBhc3NwaHJhc2VfcHJvdGVjdGlvbhgDIAEoCBIWCg5waW5fcHJvdGVjdGlvbhgEIAEoCBIUCghsYW5ndWFnZRgFIAEoCUICGAESDQoFbGFiZWwYBiABKAkSEwoLdTJmX2NvdW50ZXIYByABKA0SEwoLc2tpcF9iYWNrdXAYCCABKAgSEQoJbm9fYmFja3VwGAkgASgIEkUKC2JhY2t1cF90eXBlGAogASgOMikuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwVHlwZToFQmlwMzkSFQoNZW50cm9weV9jaGVjaxgLIAEoCBJLCg1iYWNrdXBfbWV0aG9kGAwgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwTWV0aG9kOgdEaXNwbGF5SgQIARACIvwBCgxCYWNrdXBEZXZpY2USFwoPZ3JvdXBfdGhyZXNob2xkGAEgASgNEkcKBmdyb3VwcxgCIAMoCzI3Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkJhY2t1cERldmljZS5TbGlwMzlHcm91cBJLCg1iYWNrdXBfbWV0aG9kGAMgASgOMisuaHcudHJlem9yLm1lc3NhZ2VzLm1hbmFnZW1lbnQuQmFja3VwTWV0aG9kOgdEaXNwbGF5Gj0KC1NsaXAzOUdyb3VwEhgKEG1lbWJlcl90aHJlc2hvbGQYASACKA0SFAoMbWVtYmVyX2NvdW50GAIgAigNIkIKDkVudHJvcHlSZXF1ZXN0EhoKEmVudHJvcHlfY29tbWl0bWVudBgBIAEoDBIUCgxwcmV2X2VudHJvcHkYAiABKAwiHQoKRW50cm9weUFjaxIPCgdlbnRyb3B5GAEgAigMIhMKEUVudHJvcHlDaGVja1JlYWR5Ii0KFEVudHJvcHlDaGVja0NvbnRpbnVlEhUKBmZpbmlzaBgBIAEoCDoFZmFsc2Ui4AMKDlJlY292ZXJ5RGV2aWNlEhIKCndvcmRfY291bnQYASABKA0SHQoVcGFzc3BocmFzZV9wcm90ZWN0aW9uGAIgASgIEhYKDnBpbl9wcm90ZWN0aW9uGAMgASgIEhQKCGxhbmd1YWdlGAQgASgJQgIYARINCgVsYWJlbBgFIAEoCRIYChBlbmZvcmNlX3dvcmRsaXN0GAYgASgIEl0KDGlucHV0X21ldGhvZBgIIAEoDjJHLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlY292ZXJ5RGV2aWNlLlJlY292ZXJ5RGV2aWNlSW5wdXRNZXRob2QSEwoLdTJmX2NvdW50ZXIYCSABKA0SSQoEdHlwZRgKIAEoDjIrLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlY292ZXJ5VHlwZToOTm9ybWFsUmVjb3ZlcnkSQgoNYmFja3VwX21ldGhvZBgLIAEoDjIrLmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LkJhY2t1cE1ldGhvZCI7ChlSZWNvdmVyeURldmljZUlucHV0TWV0aG9kEhIKDlNjcmFtYmxlZFdvcmRzEAASCgoGTWF0cml4EAFKBAgHEAgivwEKC1dvcmRSZXF1ZXN0EkgKBHR5cGUYASACKA4yOi5ody50cmV6b3IubWVzc2FnZXMubWFuYWdlbWVudC5Xb3JkUmVxdWVzdC5Xb3JkUmVxdWVzdFR5cGUiZgoPV29yZFJlcXVlc3RUeXBlEhkKFVdvcmRSZXF1ZXN0VHlwZV9QbGFpbhAAEhsKF1dvcmRSZXF1ZXN0VHlwZV9NYXRyaXg5EAESGwoXV29yZFJlcXVlc3RUeXBlX01hdHJpeDYQAiIXCgdXb3JkQWNrEgwKBHdvcmQYASACKAkiJAoNU2V0VTJGQ291bnRlchITCgt1MmZfY291bnRlchgBIAIoDSITChFHZXROZXh0VTJGQ291bnRlciIlCg5OZXh0VTJGQ291bnRlchITCgt1MmZfY291bnRlchgBIAIoDSIRCg9Eb1ByZWF1dGhvcml6ZWQiFgoUUHJlYXV0aG9yaXplZFJlcXVlc3QiFQoTQ2FuY2VsQXV0aG9yaXphdGlvbiLOAQoSUmVib290VG9Cb290bG9hZGVyEmIKDGJvb3RfY29tbWFuZBgBIAEoDjI9Lmh3LnRyZXpvci5tZXNzYWdlcy5tYW5hZ2VtZW50LlJlYm9vdFRvQm9vdGxvYWRlci5Cb290Q29tbWFuZDoNU1RPUF9BTkRfV0FJVBIXCg9maXJtd2FyZV9oZWFkZXIYAiABKAwiNQoLQm9vdENvbW1hbmQSEQoNU1RPUF9BTkRfV0FJVBAAEhMKD0lOU1RBTExfVVBHUkFERRABSgQIAxAEIgoKCEdldE5vbmNlIhYKBU5vbmNlEg0KBW5vbmNlGAEgAigMIiwKClVubG9ja1BhdGgSEQoJYWRkcmVzc19uGAEgAygNEgsKA21hYxgCIAEoDCIiChNVbmxvY2tlZFBhdGhSZXF1ZXN0EgsKA21hYxgBIAIoDCIUChJTaG93RGV2aWNlVHV0b3JpYWwiEgoQVW5sb2NrQm9vdGxvYWRlciIeCg1TZXRCcmlnaHRuZXNzEg0KBXZhbHVlGAEgASgNIhEKD0dldFNlcmlhbE51bWJlciIlCgxTZXJpYWxOdW1iZXISFQoNc2VyaWFsX251bWJlchgBIAIoCSqZAQoKQmFja3VwVHlwZRIJCgVCaXAzORAAEhAKDFNsaXAzOV9CYXNpYxABEhMKD1NsaXAzOV9BZHZhbmNlZBACEhwKGFNsaXAzOV9TaW5nbGVfRXh0ZW5kYWJsZRADEhsKF1NsaXAzOV9CYXNpY19FeHRlbmRhYmxlEAQSHgoaU2xpcDM5X0FkdmFuY2VkX0V4dGVuZGFibGUQBSolCgxCYWNrdXBNZXRob2QSCwoHRGlzcGxheRAAEggKBE40VzEQASpHChBTYWZldHlDaGVja0xldmVsEgoKBlN0cmljdBAAEhAKDFByb21wdEFsd2F5cxABEhUKEVByb21wdFRlbXBvcmFyaWx5EAIqPQoPRGlzcGxheVJvdGF0aW9uEgkKBU5vcnRoEAASCAoERWFzdBBaEgoKBVNvdXRoELQBEgkKBFdlc3QQjgIqMAoQSG9tZXNjcmVlbkZvcm1hdBIICgRUb2lmEAESCAoESnBlZxACEggKBFRvaUcQAypICgxSZWNvdmVyeVR5cGUSEgoOTm9ybWFsUmVjb3ZlcnkQABIKCgZEcnlSdW4QARIYChRVbmxvY2tSZXBlYXRlZEJhY2t1cBACQkIKI2NvbS5zYXRvc2hpbGFicy50cmV6b3IubGliLnByb3RvYnVmQhdUcmV6b3JNZXNzYWdlTWFuYWdlbWVudICmHQE", [file_options]); + +/** + * Describes the message hw.trezor.messages.management.Initialize. + * Use `create(InitializeSchema)` to create a new message. + */ +export const InitializeSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 0); + +/** + * Describes the message hw.trezor.messages.management.GetFeatures. + * Use `create(GetFeaturesSchema)` to create a new message. + */ +export const GetFeaturesSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 1); + +/** + * Describes the message hw.trezor.messages.management.Features. + * Use `create(FeaturesSchema)` to create a new message. + */ +export const FeaturesSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 2); + +/** + * Describes the enum hw.trezor.messages.management.Features.BackupAvailability. + */ +export const Features_BackupAvailabilitySchema = /*@__PURE__*/ + enumDesc(file_messages_management, 2, 0); + +/** + * @generated from enum hw.trezor.messages.management.Features.BackupAvailability + */ +export const Features_BackupAvailability = /*@__PURE__*/ + tsEnum(Features_BackupAvailabilitySchema); + +/** + * Describes the enum hw.trezor.messages.management.Features.RecoveryStatus. + */ +export const Features_RecoveryStatusSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 2, 1); + +/** + * @generated from enum hw.trezor.messages.management.Features.RecoveryStatus + */ +export const Features_RecoveryStatus = /*@__PURE__*/ + tsEnum(Features_RecoveryStatusSchema); + +/** + * Describes the enum hw.trezor.messages.management.Features.Capability. + */ +export const Features_CapabilitySchema = /*@__PURE__*/ + enumDesc(file_messages_management, 2, 2); + +/** + * @generated from enum hw.trezor.messages.management.Features.Capability + */ +export const Features_Capability = /*@__PURE__*/ + tsEnum(Features_CapabilitySchema); + +/** + * Describes the message hw.trezor.messages.management.LockDevice. + * Use `create(LockDeviceSchema)` to create a new message. + */ +export const LockDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 3); + +/** + * Describes the message hw.trezor.messages.management.SetBusy. + * Use `create(SetBusySchema)` to create a new message. + */ +export const SetBusySchema = /*@__PURE__*/ + messageDesc(file_messages_management, 4); + +/** + * Describes the message hw.trezor.messages.management.EndSession. + * Use `create(EndSessionSchema)` to create a new message. + */ +export const EndSessionSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 5); + +/** + * Describes the message hw.trezor.messages.management.ApplySettings. + * Use `create(ApplySettingsSchema)` to create a new message. + */ +export const ApplySettingsSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 6); + +/** + * Describes the message hw.trezor.messages.management.ChangeLanguage. + * Use `create(ChangeLanguageSchema)` to create a new message. + */ +export const ChangeLanguageSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 7); + +/** + * Describes the message hw.trezor.messages.management.DataChunkRequest. + * Use `create(DataChunkRequestSchema)` to create a new message. + */ +export const DataChunkRequestSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 8); + +/** + * Describes the message hw.trezor.messages.management.DataChunkAck. + * Use `create(DataChunkAckSchema)` to create a new message. + */ +export const DataChunkAckSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 9); + +/** + * Describes the message hw.trezor.messages.management.ApplyFlags. + * Use `create(ApplyFlagsSchema)` to create a new message. + */ +export const ApplyFlagsSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 10); + +/** + * Describes the message hw.trezor.messages.management.ChangePin. + * Use `create(ChangePinSchema)` to create a new message. + */ +export const ChangePinSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 11); + +/** + * Describes the message hw.trezor.messages.management.ChangeWipeCode. + * Use `create(ChangeWipeCodeSchema)` to create a new message. + */ +export const ChangeWipeCodeSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 12); + +/** + * Describes the message hw.trezor.messages.management.SdProtect. + * Use `create(SdProtectSchema)` to create a new message. + */ +export const SdProtectSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 13); + +/** + * Describes the enum hw.trezor.messages.management.SdProtect.SdProtectOperationType. + */ +export const SdProtect_SdProtectOperationTypeSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 13, 0); + +/** + * * + * Structure representing SD card protection operation + * + * @generated from enum hw.trezor.messages.management.SdProtect.SdProtectOperationType + */ +export const SdProtect_SdProtectOperationType = /*@__PURE__*/ + tsEnum(SdProtect_SdProtectOperationTypeSchema); + +/** + * Describes the message hw.trezor.messages.management.Ping. + * Use `create(PingSchema)` to create a new message. + */ +export const PingSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 14); + +/** + * Describes the message hw.trezor.messages.management.Cancel. + * Use `create(CancelSchema)` to create a new message. + */ +export const CancelSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 15); + +/** + * Describes the message hw.trezor.messages.management.GetEntropy. + * Use `create(GetEntropySchema)` to create a new message. + */ +export const GetEntropySchema = /*@__PURE__*/ + messageDesc(file_messages_management, 16); + +/** + * Describes the message hw.trezor.messages.management.Entropy. + * Use `create(EntropySchema)` to create a new message. + */ +export const EntropySchema = /*@__PURE__*/ + messageDesc(file_messages_management, 17); + +/** + * Describes the message hw.trezor.messages.management.GetFirmwareHash. + * Use `create(GetFirmwareHashSchema)` to create a new message. + */ +export const GetFirmwareHashSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 18); + +/** + * Describes the message hw.trezor.messages.management.FirmwareHash. + * Use `create(FirmwareHashSchema)` to create a new message. + */ +export const FirmwareHashSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 19); + +/** + * Describes the message hw.trezor.messages.management.AuthenticateDevice. + * Use `create(AuthenticateDeviceSchema)` to create a new message. + */ +export const AuthenticateDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 20); + +/** + * Describes the message hw.trezor.messages.management.AuthenticityProof. + * Use `create(AuthenticityProofSchema)` to create a new message. + */ +export const AuthenticityProofSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 21); + +/** + * Describes the message hw.trezor.messages.management.AuthenticityProofSizes. + * Use `create(AuthenticityProofSizesSchema)` to create a new message. + */ +export const AuthenticityProofSizesSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 22); + +/** + * Describes the message hw.trezor.messages.management.GetAuthenticityProofChunk. + * Use `create(GetAuthenticityProofChunkSchema)` to create a new message. + */ +export const GetAuthenticityProofChunkSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 23); + +/** + * Describes the enum hw.trezor.messages.management.GetAuthenticityProofChunk.AuthenticityProofType. + */ +export const GetAuthenticityProofChunk_AuthenticityProofTypeSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 23, 0); + +/** + * @generated from enum hw.trezor.messages.management.GetAuthenticityProofChunk.AuthenticityProofType + */ +export const GetAuthenticityProofChunk_AuthenticityProofType = /*@__PURE__*/ + tsEnum(GetAuthenticityProofChunk_AuthenticityProofTypeSchema); + +/** + * Describes the message hw.trezor.messages.management.AuthenticityProofChunk. + * Use `create(AuthenticityProofChunkSchema)` to create a new message. + */ +export const AuthenticityProofChunkSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 24); + +/** + * Describes the message hw.trezor.messages.management.WipeDevice. + * Use `create(WipeDeviceSchema)` to create a new message. + */ +export const WipeDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 25); + +/** + * Describes the message hw.trezor.messages.management.LoadDevice. + * Use `create(LoadDeviceSchema)` to create a new message. + */ +export const LoadDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 26); + +/** + * Describes the message hw.trezor.messages.management.ResetDevice. + * Use `create(ResetDeviceSchema)` to create a new message. + */ +export const ResetDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 27); + +/** + * Describes the message hw.trezor.messages.management.BackupDevice. + * Use `create(BackupDeviceSchema)` to create a new message. + */ +export const BackupDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 28); + +/** + * Describes the message hw.trezor.messages.management.BackupDevice.Slip39Group. + * Use `create(BackupDevice_Slip39GroupSchema)` to create a new message. + */ +export const BackupDevice_Slip39GroupSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 28, 0); + +/** + * Describes the message hw.trezor.messages.management.EntropyRequest. + * Use `create(EntropyRequestSchema)` to create a new message. + */ +export const EntropyRequestSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 29); + +/** + * Describes the message hw.trezor.messages.management.EntropyAck. + * Use `create(EntropyAckSchema)` to create a new message. + */ +export const EntropyAckSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 30); + +/** + * Describes the message hw.trezor.messages.management.EntropyCheckReady. + * Use `create(EntropyCheckReadySchema)` to create a new message. + */ +export const EntropyCheckReadySchema = /*@__PURE__*/ + messageDesc(file_messages_management, 31); + +/** + * Describes the message hw.trezor.messages.management.EntropyCheckContinue. + * Use `create(EntropyCheckContinueSchema)` to create a new message. + */ +export const EntropyCheckContinueSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 32); + +/** + * Describes the message hw.trezor.messages.management.RecoveryDevice. + * Use `create(RecoveryDeviceSchema)` to create a new message. + */ +export const RecoveryDeviceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 33); + +/** + * Describes the enum hw.trezor.messages.management.RecoveryDevice.RecoveryDeviceInputMethod. + */ +export const RecoveryDevice_RecoveryDeviceInputMethodSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 33, 0); + +/** + * * + * Type of recovery procedure. These should be used as bitmask, e.g., + * `RecoveryDeviceInputMethod_ScrambledWords | RecoveryDeviceInputMethod_Matrix` + * listing every method supported by the host computer. + * + * Note that Matrix recovery is the recommended recovery method. ScrambledWords + * is allowed only for 24 word recovery, unless safety checks are disabled. + * + * @generated from enum hw.trezor.messages.management.RecoveryDevice.RecoveryDeviceInputMethod + */ +export const RecoveryDevice_RecoveryDeviceInputMethod = /*@__PURE__*/ + tsEnum(RecoveryDevice_RecoveryDeviceInputMethodSchema); + +/** + * Describes the message hw.trezor.messages.management.WordRequest. + * Use `create(WordRequestSchema)` to create a new message. + */ +export const WordRequestSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 34); + +/** + * Describes the enum hw.trezor.messages.management.WordRequest.WordRequestType. + */ +export const WordRequest_WordRequestTypeSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 34, 0); + +/** + * * + * Type of Recovery Word request + * + * @generated from enum hw.trezor.messages.management.WordRequest.WordRequestType + */ +export const WordRequest_WordRequestType = /*@__PURE__*/ + tsEnum(WordRequest_WordRequestTypeSchema); + +/** + * Describes the message hw.trezor.messages.management.WordAck. + * Use `create(WordAckSchema)` to create a new message. + */ +export const WordAckSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 35); + +/** + * Describes the message hw.trezor.messages.management.SetU2FCounter. + * Use `create(SetU2FCounterSchema)` to create a new message. + */ +export const SetU2FCounterSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 36); + +/** + * Describes the message hw.trezor.messages.management.GetNextU2FCounter. + * Use `create(GetNextU2FCounterSchema)` to create a new message. + */ +export const GetNextU2FCounterSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 37); + +/** + * Describes the message hw.trezor.messages.management.NextU2FCounter. + * Use `create(NextU2FCounterSchema)` to create a new message. + */ +export const NextU2FCounterSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 38); + +/** + * Describes the message hw.trezor.messages.management.DoPreauthorized. + * Use `create(DoPreauthorizedSchema)` to create a new message. + */ +export const DoPreauthorizedSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 39); + +/** + * Describes the message hw.trezor.messages.management.PreauthorizedRequest. + * Use `create(PreauthorizedRequestSchema)` to create a new message. + */ +export const PreauthorizedRequestSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 40); + +/** + * Describes the message hw.trezor.messages.management.CancelAuthorization. + * Use `create(CancelAuthorizationSchema)` to create a new message. + */ +export const CancelAuthorizationSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 41); + +/** + * Describes the message hw.trezor.messages.management.RebootToBootloader. + * Use `create(RebootToBootloaderSchema)` to create a new message. + */ +export const RebootToBootloaderSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 42); + +/** + * Describes the enum hw.trezor.messages.management.RebootToBootloader.BootCommand. + */ +export const RebootToBootloader_BootCommandSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 42, 0); + +/** + * @generated from enum hw.trezor.messages.management.RebootToBootloader.BootCommand + */ +export const RebootToBootloader_BootCommand = /*@__PURE__*/ + tsEnum(RebootToBootloader_BootCommandSchema); + +/** + * Describes the message hw.trezor.messages.management.GetNonce. + * Use `create(GetNonceSchema)` to create a new message. + */ +export const GetNonceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 43); + +/** + * Describes the message hw.trezor.messages.management.Nonce. + * Use `create(NonceSchema)` to create a new message. + */ +export const NonceSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 44); + +/** + * Describes the message hw.trezor.messages.management.UnlockPath. + * Use `create(UnlockPathSchema)` to create a new message. + */ +export const UnlockPathSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 45); + +/** + * Describes the message hw.trezor.messages.management.UnlockedPathRequest. + * Use `create(UnlockedPathRequestSchema)` to create a new message. + */ +export const UnlockedPathRequestSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 46); + +/** + * Describes the message hw.trezor.messages.management.ShowDeviceTutorial. + * Use `create(ShowDeviceTutorialSchema)` to create a new message. + */ +export const ShowDeviceTutorialSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 47); + +/** + * Describes the message hw.trezor.messages.management.UnlockBootloader. + * Use `create(UnlockBootloaderSchema)` to create a new message. + */ +export const UnlockBootloaderSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 48); + +/** + * Describes the message hw.trezor.messages.management.SetBrightness. + * Use `create(SetBrightnessSchema)` to create a new message. + */ +export const SetBrightnessSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 49); + +/** + * Describes the message hw.trezor.messages.management.GetSerialNumber. + * Use `create(GetSerialNumberSchema)` to create a new message. + */ +export const GetSerialNumberSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 50); + +/** + * Describes the message hw.trezor.messages.management.SerialNumber. + * Use `create(SerialNumberSchema)` to create a new message. + */ +export const SerialNumberSchema = /*@__PURE__*/ + messageDesc(file_messages_management, 51); + +/** + * Describes the enum hw.trezor.messages.management.BackupType. + */ +export const BackupTypeSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 0); + +/** + * * + * Type of the mnemonic backup given/received by the device during reset/recovery. + * + * @generated from enum hw.trezor.messages.management.BackupType + */ +export const BackupType = /*@__PURE__*/ + tsEnum(BackupTypeSchema); + +/** + * Describes the enum hw.trezor.messages.management.BackupMethod. + */ +export const BackupMethodSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 1); + +/** + * * + * Backup method of the mnemonic shares. + * + * @generated from enum hw.trezor.messages.management.BackupMethod + */ +export const BackupMethod = /*@__PURE__*/ + tsEnum(BackupMethodSchema); + +/** + * Describes the enum hw.trezor.messages.management.SafetyCheckLevel. + */ +export const SafetyCheckLevelSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 2); + +/** + * * + * Level of safety checks for unsafe actions like spending from invalid path namespace or setting high transaction fee. + * + * @generated from enum hw.trezor.messages.management.SafetyCheckLevel + */ +export const SafetyCheckLevel = /*@__PURE__*/ + tsEnum(SafetyCheckLevelSchema); + +/** + * Describes the enum hw.trezor.messages.management.DisplayRotation. + */ +export const DisplayRotationSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 3); + +/** + * * + * Allowed display rotation angles (in degrees from North) + * + * @generated from enum hw.trezor.messages.management.DisplayRotation + */ +export const DisplayRotation = /*@__PURE__*/ + tsEnum(DisplayRotationSchema); + +/** + * Describes the enum hw.trezor.messages.management.HomescreenFormat. + */ +export const HomescreenFormatSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 4); + +/** + * * + * Format of the homescreen image + * + * @generated from enum hw.trezor.messages.management.HomescreenFormat + */ +export const HomescreenFormat = /*@__PURE__*/ + tsEnum(HomescreenFormatSchema); + +/** + * Describes the enum hw.trezor.messages.management.RecoveryType. + */ +export const RecoveryTypeSchema = /*@__PURE__*/ + enumDesc(file_messages_management, 5); + +/** + * @generated from enum hw.trezor.messages.management.RecoveryType + */ +export const RecoveryType = /*@__PURE__*/ + tsEnum(RecoveryTypeSchema); diff --git a/packages/hwk-trezor-protobuf/src/definitions/messages_pb.js b/packages/hwk-trezor-protobuf/src/definitions/messages_pb.js index 5992f11a1..4139124f3 100644 --- a/packages/hwk-trezor-protobuf/src/definitions/messages_pb.js +++ b/packages/hwk-trezor-protobuf/src/definitions/messages_pb.js @@ -1,11 +1,27 @@ // @generated by protoc-gen-es v2.11.0 with parameter "target=js,json_types=true" // @generated from file messages.proto (package hw.trezor.messages, syntax proto2) -import { enumDesc, fileDesc, tsEnum } from '@bufbuild/protobuf/codegenv2'; - -import { file_options } from './options_pb'; -export const file_messages = fileDesc( - 'Cg5tZXNzYWdlcy5wcm90bxISaHcudHJlem9yLm1lc3NhZ2VzKt5jCgtNZXNzYWdlVHlwZRIoChZNZXNzYWdlVHlwZV9Jbml0aWFsaXplEAAaDJC1GAGwtRgBgKYdARIeChBNZXNzYWdlVHlwZV9QaW5nEAEaCJC1GAGAph0BEiUKE01lc3NhZ2VUeXBlX1N1Y2Nlc3MQAhoMmLUYAai1GAGAph0BEiUKE01lc3NhZ2VUeXBlX0ZhaWx1cmUQAxoMmLUYAai1GAGAph0BEiMKFU1lc3NhZ2VUeXBlX0NoYW5nZVBpbhAEGgiQtRgBgKYdARIkChZNZXNzYWdlVHlwZV9XaXBlRGV2aWNlEAUaCJC1GAGAph0BEiQKFk1lc3NhZ2VUeXBlX0dldEVudHJvcHkQCRoIkLUYAYCmHQESIQoTTWVzc2FnZVR5cGVfRW50cm9weRAKGgiYtRgBgKYdARIkChZNZXNzYWdlVHlwZV9Mb2FkRGV2aWNlEA0aCJC1GAGAph0BEiUKF01lc3NhZ2VUeXBlX1Jlc2V0RGV2aWNlEA4aCJC1GAGAph0BEiEKE01lc3NhZ2VUeXBlX1NldEJ1c3kQEBoIkLUYAYCmHQESIgoUTWVzc2FnZVR5cGVfRmVhdHVyZXMQERoImLUYAYCmHQESKgocTWVzc2FnZVR5cGVfUGluTWF0cml4UmVxdWVzdBASGgiYtRgBgKYdARIuChhNZXNzYWdlVHlwZV9QaW5NYXRyaXhBY2sQExoQkLUYAbC1GAHAtRgBgKYdARIkChJNZXNzYWdlVHlwZV9DYW5jZWwQFBoMkLUYAbC1GAGAph0BEiQKFk1lc3NhZ2VUeXBlX0xvY2tEZXZpY2UQGBoIkLUYAYCmHQESJwoZTWVzc2FnZVR5cGVfQXBwbHlTZXR0aW5ncxAZGgiQtRgBgKYdARInChlNZXNzYWdlVHlwZV9CdXR0b25SZXF1ZXN0EBoaCJi1GAGAph0BEisKFU1lc3NhZ2VUeXBlX0J1dHRvbkFjaxAbGhCQtRgBsLUYAcC1GAGAph0BEiQKFk1lc3NhZ2VUeXBlX0FwcGx5RmxhZ3MQHBoIkLUYAYCmHQESIgoUTWVzc2FnZVR5cGVfR2V0Tm9uY2UQHxoIkLUYAYCmHQESHwoRTWVzc2FnZVR5cGVfTm9uY2UQIRoImLUYAYCmHQESJgoYTWVzc2FnZVR5cGVfQmFja3VwRGV2aWNlECIaCJC1GAGAph0BEigKGk1lc3NhZ2VUeXBlX0VudHJvcHlSZXF1ZXN0ECMaCJi1GAGAph0BEiQKFk1lc3NhZ2VUeXBlX0VudHJvcHlBY2sQJBoIkLUYAYCmHQESKAoaTWVzc2FnZVR5cGVfUGF5bWVudFJlcXVlc3QQJRoIkLUYAYCmHQESLAodTWVzc2FnZVR5cGVfRW50cm9weUNoZWNrUmVhZHkQ4gcaCJi1GAGAph0BEi8KIE1lc3NhZ2VUeXBlX0VudHJvcHlDaGVja0NvbnRpbnVlEOMHGgiQtRgBgKYdARIrCh1NZXNzYWdlVHlwZV9QYXNzcGhyYXNlUmVxdWVzdBApGgiYtRgBgKYdARIvChlNZXNzYWdlVHlwZV9QYXNzcGhyYXNlQWNrECoaEJC1GAGwtRgBwLUYAYCmHQESKAoaTWVzc2FnZVR5cGVfUmVjb3ZlcnlEZXZpY2UQLRoIkLUYAYCmHQESJQoXTWVzc2FnZVR5cGVfV29yZFJlcXVlc3QQLhoImLUYAYCmHQESIQoTTWVzc2FnZVR5cGVfV29yZEFjaxAvGgiQtRgBgKYdARIlChdNZXNzYWdlVHlwZV9HZXRGZWF0dXJlcxA3GgiQtRgBgKYdARIjChVNZXNzYWdlVHlwZV9TZFByb3RlY3QQTxoIkLUYAYCmHQESKAoaTWVzc2FnZVR5cGVfQ2hhbmdlV2lwZUNvZGUQUhoIkLUYAYCmHQESJAoWTWVzc2FnZVR5cGVfRW5kU2Vzc2lvbhBTGgiQtRgBgKYdARIpChtNZXNzYWdlVHlwZV9Eb1ByZWF1dGhvcml6ZWQQVBoIkLUYAYCmHQESLgogTWVzc2FnZVR5cGVfUHJlYXV0aG9yaXplZFJlcXVlc3QQVRoImLUYAYCmHQESLQofTWVzc2FnZVR5cGVfQ2FuY2VsQXV0aG9yaXphdGlvbhBWGgiQtRgBgKYdARIsCh5NZXNzYWdlVHlwZV9SZWJvb3RUb0Jvb3Rsb2FkZXIQVxoIkLUYAYCmHQESKQobTWVzc2FnZVR5cGVfR2V0RmlybXdhcmVIYXNoEFgaCJC1GAGAph0BEiYKGE1lc3NhZ2VUeXBlX0Zpcm13YXJlSGFzaBBZGgiYtRgBgKYdARIkChZNZXNzYWdlVHlwZV9VbmxvY2tQYXRoEF0aCJC1GAGAph0BEi0KH01lc3NhZ2VUeXBlX1VubG9ja2VkUGF0aFJlcXVlc3QQXhoImLUYAYCmHQESLAoeTWVzc2FnZVR5cGVfU2hvd0RldmljZVR1dG9yaWFsEF8aCJC1GAGAph0BEioKHE1lc3NhZ2VUeXBlX1VubG9ja0Jvb3Rsb2FkZXIQYBoIkLUYAYCmHQESLAoeTWVzc2FnZVR5cGVfQXV0aGVudGljYXRlRGV2aWNlEGEaCJi1GAGAph0BEisKHU1lc3NhZ2VUeXBlX0F1dGhlbnRpY2l0eVByb29mEGIaCJC1GAGAph0BEikKGk1lc3NhZ2VUeXBlX0NoYW5nZUxhbmd1YWdlEN4HGgiQtRgBgKYdARIrChxNZXNzYWdlVHlwZV9EYXRhQ2h1bmtSZXF1ZXN0EN8HGgiYtRgBgKYdARInChhNZXNzYWdlVHlwZV9EYXRhQ2h1bmtBY2sQ4AcaCJC1GAGAph0BEigKGU1lc3NhZ2VUeXBlX1NldEJyaWdodG5lc3MQ4QcaCJC1GAGAph0BEioKG01lc3NhZ2VUeXBlX0dldFNlcmlhbE51bWJlchDkBxoIkLUYAYCmHQESJwoYTWVzc2FnZVR5cGVfU2VyaWFsTnVtYmVyEOUHGgiYtRgBgKYdARIjChlNZXNzYWdlVHlwZV9TZXRVMkZDb3VudGVyED8aBJC1GAESJwodTWVzc2FnZVR5cGVfR2V0TmV4dFUyRkNvdW50ZXIQUBoEkLUYARIkChpNZXNzYWdlVHlwZV9OZXh0VTJGQ291bnRlchBRGgSYtRgBEjUKLU1lc3NhZ2VUeXBlX0RlcHJlY2F0ZWRfUGFzc3BocmFzZVN0YXRlUmVxdWVzdBBNGgIIARIxCilNZXNzYWdlVHlwZV9EZXByZWNhdGVkX1Bhc3NwaHJhc2VTdGF0ZUFjaxBOGgIIARIrChlNZXNzYWdlVHlwZV9GaXJtd2FyZUVyYXNlEAYaDJC1GAG4tRgBgKYdARIsChpNZXNzYWdlVHlwZV9GaXJtd2FyZVVwbG9hZBAHGgyQtRgBuLUYAYCmHQESLQobTWVzc2FnZVR5cGVfRmlybXdhcmVSZXF1ZXN0EAgaDJi1GAG4tRgBgKYdARIoChZNZXNzYWdlVHlwZV9Qcm9kVGVzdFQxECAaDJC1GAG4tRgBgKYdARIkChVNZXNzYWdlVHlwZV9CbGVVbnBhaXIQwT4aCJC1GAGAph0BEiYKGE1lc3NhZ2VUeXBlX0dldFB1YmxpY0tleRALGgiQtRgBgKYdARIjChVNZXNzYWdlVHlwZV9QdWJsaWNLZXkQDBoImLUYAYCmHQESIAoSTWVzc2FnZVR5cGVfU2lnblR4EA8aCJC1GAGAph0BEiMKFU1lc3NhZ2VUeXBlX1R4UmVxdWVzdBAVGgiYtRgBgKYdARIfChFNZXNzYWdlVHlwZV9UeEFjaxAWGgiQtRgBgKYdARIkChZNZXNzYWdlVHlwZV9HZXRBZGRyZXNzEB0aCJC1GAGAph0BEiEKE01lc3NhZ2VUeXBlX0FkZHJlc3MQHhoImLUYAYCmHQESJQoXTWVzc2FnZVR5cGVfU2lnbk1lc3NhZ2UQJhoIkLUYAYCmHQESJwoZTWVzc2FnZVR5cGVfVmVyaWZ5TWVzc2FnZRAnGgiQtRgBgKYdARIqChxNZXNzYWdlVHlwZV9NZXNzYWdlU2lnbmF0dXJlECgaCJi1GAGAph0BEigKGk1lc3NhZ2VUeXBlX0dldE93bmVyc2hpcElkECsaCJC1GAGAph0BEiUKF01lc3NhZ2VUeXBlX093bmVyc2hpcElkECwaCJi1GAGAph0BEisKHU1lc3NhZ2VUeXBlX0dldE93bmVyc2hpcFByb29mEDEaCJC1GAGAph0BEigKGk1lc3NhZ2VUeXBlX093bmVyc2hpcFByb29mEDIaCJi1GAGAph0BEisKHU1lc3NhZ2VUeXBlX0F1dGhvcml6ZUNvaW5Kb2luEDMaCJC1GAGAph0BEigKGk1lc3NhZ2VUeXBlX0NpcGhlcktleVZhbHVlEBcaCJC1GAGAph0BEioKHE1lc3NhZ2VUeXBlX0NpcGhlcmVkS2V5VmFsdWUQMBoImLUYAYCmHQESJgoYTWVzc2FnZVR5cGVfU2lnbklkZW50aXR5EDUaCJC1GAGAph0BEigKGk1lc3NhZ2VUeXBlX1NpZ25lZElkZW50aXR5EDYaCJi1GAGAph0BEisKHU1lc3NhZ2VUeXBlX0dldEVDREhTZXNzaW9uS2V5ED0aCJC1GAGAph0BEigKGk1lc3NhZ2VUeXBlX0VDREhTZXNzaW9uS2V5ED4aCJi1GAGAph0BEi0KH01lc3NhZ2VUeXBlX1BheW1lbnROb3RpZmljYXRpb24QNBoIkLUYAYCmHQESMwodTWVzc2FnZVR5cGVfRGVidWdMaW5rRGVjaXNpb24QZBoQoLUYAbC1GAHAtRgBgKYdARIvCh1NZXNzYWdlVHlwZV9EZWJ1Z0xpbmtHZXRTdGF0ZRBlGgygtRgBsLUYAYCmHQESKAoaTWVzc2FnZVR5cGVfRGVidWdMaW5rU3RhdGUQZhoIqLUYAYCmHQESJwoZTWVzc2FnZVR5cGVfRGVidWdMaW5rU3RvcBBnGgigtRgBgKYdARImChhNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtMb2cQaBoIqLUYAYCmHQESLQofTWVzc2FnZVR5cGVfRGVidWdMaW5rTWVtb3J5UmVhZBBuGgigtRgBgKYdARIpChtNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtNZW1vcnkQbxoIqLUYAYCmHQESLgogTWVzc2FnZVR5cGVfRGVidWdMaW5rTWVtb3J5V3JpdGUQcBoIoLUYAYCmHQESLQofTWVzc2FnZVR5cGVfRGVidWdMaW5rRmxhc2hFcmFzZRBxGgigtRgBgKYdARIqChtNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtMYXlvdXQQqUYaCKi1GAGAph0BEjAKIU1lc3NhZ2VUeXBlX0RlYnVnTGlua1Jlc2VlZFJhbmRvbRCqRhoIoLUYAYCmHQESMAohTWVzc2FnZVR5cGVfRGVidWdMaW5rUmVjb3JkU2NyZWVuEKtGGgigtRgBgKYdARIvCiBNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtFcmFzZVNkQ2FyZBCtRhoIoLUYAYCmHQESLwogTWVzc2FnZVR5cGVfRGVidWdMaW5rV2F0Y2hMYXlvdXQQrkYaCKC1GAGAph0BEjQKJU1lc3NhZ2VUeXBlX0RlYnVnTGlua1Jlc2V0RGVidWdFdmVudHMQr0YaCKC1GAGAph0BEjMKJE1lc3NhZ2VUeXBlX0RlYnVnTGlua09wdGlnYVNldFNlY01heBCwRhoIoLUYAYCmHQESLQoeTWVzc2FnZVR5cGVfRGVidWdMaW5rR2V0R2NJbmZvELFGGgigtRgBgKYdARIqChtNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtHY0luZm8QskYaCKi1GAGAph0BEjIKI01lc3NhZ2VUeXBlX0RlYnVnTGlua0dldFBhaXJpbmdJbmZvELNGGgigtRgBgKYdARIvCiBNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtQYWlyaW5nSW5mbxC0RhoIqLUYAYCmHQESMAohTWVzc2FnZVR5cGVfRGVidWdMaW5rU2V0TG9nRmlsdGVyELVGGgigtRgBgKYdARIxCiJNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtONFcxQ29ubmVjdGVkELZGGgigtRgBgKYdARItCh5NZXNzYWdlVHlwZV9EZWJ1Z0xpbmtONFcxV3JpdGUQt0YaCKi1GAGAph0BEiwKHU1lc3NhZ2VUeXBlX0RlYnVnTGlua040VzFSZWFkELhGGgiotRgBgKYdARIwCiFNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtONFcxUmVzcG9uc2UQuUYaCKC1GAGAph0BEjMKJE1lc3NhZ2VUeXBlX0RlYnVnTGlua1NldEJhdHRlcnlTdGF0ZRC6RhoIoLUYAYCmHQESKwogTWVzc2FnZVR5cGVfRXRoZXJldW1HZXRQdWJsaWNLZXkQwgMaBJC1GAESKAodTWVzc2FnZVR5cGVfRXRoZXJldW1QdWJsaWNLZXkQwwMaBJi1GAESKAoeTWVzc2FnZVR5cGVfRXRoZXJldW1HZXRBZGRyZXNzEDgaBJC1GAESJQobTWVzc2FnZVR5cGVfRXRoZXJldW1BZGRyZXNzEDkaBJi1GAESJAoaTWVzc2FnZVR5cGVfRXRoZXJldW1TaWduVHgQOhoEkLUYARIsCiFNZXNzYWdlVHlwZV9FdGhlcmV1bVNpZ25UeEVJUDE1NTkQxAMaBJC1GAESJwodTWVzc2FnZVR5cGVfRXRoZXJldW1UeFJlcXVlc3QQOxoEmLUYARIjChlNZXNzYWdlVHlwZV9FdGhlcmV1bVR4QWNrEDwaBJC1GAESKQofTWVzc2FnZVR5cGVfRXRoZXJldW1TaWduTWVzc2FnZRBAGgSQtRgBEisKIU1lc3NhZ2VUeXBlX0V0aGVyZXVtVmVyaWZ5TWVzc2FnZRBBGgSQtRgBEi4KJE1lc3NhZ2VUeXBlX0V0aGVyZXVtTWVzc2FnZVNpZ25hdHVyZRBCGgSYtRgBEiwKIU1lc3NhZ2VUeXBlX0V0aGVyZXVtU2lnblR5cGVkRGF0YRDQAxoEkLUYARI1CipNZXNzYWdlVHlwZV9FdGhlcmV1bVR5cGVkRGF0YVN0cnVjdFJlcXVlc3QQ0QMaBJi1GAESMQomTWVzc2FnZVR5cGVfRXRoZXJldW1UeXBlZERhdGFTdHJ1Y3RBY2sQ0gMaBJC1GAESNAopTWVzc2FnZVR5cGVfRXRoZXJldW1UeXBlZERhdGFWYWx1ZVJlcXVlc3QQ0wMaBJi1GAESMAolTWVzc2FnZVR5cGVfRXRoZXJldW1UeXBlZERhdGFWYWx1ZUFjaxDUAxoEkLUYARIxCiZNZXNzYWdlVHlwZV9FdGhlcmV1bVR5cGVkRGF0YVNpZ25hdHVyZRDVAxoEmLUYARIsCiFNZXNzYWdlVHlwZV9FdGhlcmV1bVNpZ25UeXBlZEhhc2gQ1gMaBJC1GAESIwoZTWVzc2FnZVR5cGVfTkVNR2V0QWRkcmVzcxBDGgSQtRgBEiAKFk1lc3NhZ2VUeXBlX05FTUFkZHJlc3MQRBoEmLUYARIfChVNZXNzYWdlVHlwZV9ORU1TaWduVHgQRRoEkLUYARIhChdNZXNzYWdlVHlwZV9ORU1TaWduZWRUeBBGGgSYtRgBEicKHU1lc3NhZ2VUeXBlX05FTURlY3J5cHRNZXNzYWdlEEsaBJC1GAESKQofTWVzc2FnZVR5cGVfTkVNRGVjcnlwdGVkTWVzc2FnZRBMGgSYtRgBEiYKG01lc3NhZ2VUeXBlX1Rlem9zR2V0QWRkcmVzcxCWARoEkLUYARIjChhNZXNzYWdlVHlwZV9UZXpvc0FkZHJlc3MQlwEaBJi1GAESIgoXTWVzc2FnZVR5cGVfVGV6b3NTaWduVHgQmAEaBJC1GAESJAoZTWVzc2FnZVR5cGVfVGV6b3NTaWduZWRUeBCZARoEmLUYARIoCh1NZXNzYWdlVHlwZV9UZXpvc0dldFB1YmxpY0tleRCaARoEkLUYARIlChpNZXNzYWdlVHlwZV9UZXpvc1B1YmxpY0tleRCbARoEmLUYARIkChlNZXNzYWdlVHlwZV9TdGVsbGFyU2lnblR4EMoBGgSQtRgBEikKHk1lc3NhZ2VUeXBlX1N0ZWxsYXJUeE9wUmVxdWVzdBDLARoEmLUYARIoCh1NZXNzYWdlVHlwZV9TdGVsbGFyR2V0QWRkcmVzcxDPARoEkLUYARIlChpNZXNzYWdlVHlwZV9TdGVsbGFyQWRkcmVzcxDQARoEmLUYARItCiJNZXNzYWdlVHlwZV9TdGVsbGFyQ3JlYXRlQWNjb3VudE9wENIBGgSQtRgBEicKHE1lc3NhZ2VUeXBlX1N0ZWxsYXJQYXltZW50T3AQ0wEaBJC1GAESOAotTWVzc2FnZVR5cGVfU3RlbGxhclBhdGhQYXltZW50U3RyaWN0UmVjZWl2ZU9wENQBGgSQtRgBEi8KJE1lc3NhZ2VUeXBlX1N0ZWxsYXJNYW5hZ2VTZWxsT2ZmZXJPcBDVARoEkLUYARI2CitNZXNzYWdlVHlwZV9TdGVsbGFyQ3JlYXRlUGFzc2l2ZVNlbGxPZmZlck9wENYBGgSQtRgBEioKH01lc3NhZ2VUeXBlX1N0ZWxsYXJTZXRPcHRpb25zT3AQ1wEaBJC1GAESKwogTWVzc2FnZVR5cGVfU3RlbGxhckNoYW5nZVRydXN0T3AQ2AEaBJC1GAESKgofTWVzc2FnZVR5cGVfU3RlbGxhckFsbG93VHJ1c3RPcBDZARoEkLUYARIsCiFNZXNzYWdlVHlwZV9TdGVsbGFyQWNjb3VudE1lcmdlT3AQ2gEaBJC1GAESKgofTWVzc2FnZVR5cGVfU3RlbGxhck1hbmFnZURhdGFPcBDcARoEkLUYARIsCiFNZXNzYWdlVHlwZV9TdGVsbGFyQnVtcFNlcXVlbmNlT3AQ3QEaBJC1GAESLgojTWVzc2FnZVR5cGVfU3RlbGxhck1hbmFnZUJ1eU9mZmVyT3AQ3gEaBJC1GAESNQoqTWVzc2FnZVR5cGVfU3RlbGxhclBhdGhQYXltZW50U3RyaWN0U2VuZE9wEN8BGgSQtRgBEjUKKk1lc3NhZ2VUeXBlX1N0ZWxsYXJDbGFpbUNsYWltYWJsZUJhbGFuY2VPcBDhARoEkLUYARImChtNZXNzYWdlVHlwZV9TdGVsbGFyU2lnbmVkVHgQ5gEaBJi1GAESKgofTWVzc2FnZVR5cGVfQ2FyZGFub0dldFB1YmxpY0tleRCxAhoEkLUYARInChxNZXNzYWdlVHlwZV9DYXJkYW5vUHVibGljS2V5ELICGgSYtRgBEigKHU1lc3NhZ2VUeXBlX0NhcmRhbm9HZXRBZGRyZXNzELMCGgSQtRgBEiUKGk1lc3NhZ2VUeXBlX0NhcmRhbm9BZGRyZXNzELQCGgSYtRgBEicKHE1lc3NhZ2VUeXBlX0NhcmRhbm9UeEl0ZW1BY2sQuQIaBJi1GAESNwosTWVzc2FnZVR5cGVfQ2FyZGFub1R4QXV4aWxpYXJ5RGF0YVN1cHBsZW1lbnQQugIaBJi1GAESLgojTWVzc2FnZVR5cGVfQ2FyZGFub1R4V2l0bmVzc1JlcXVlc3QQuwIaBJC1GAESLwokTWVzc2FnZVR5cGVfQ2FyZGFub1R4V2l0bmVzc1Jlc3BvbnNlELwCGgSYtRgBEicKHE1lc3NhZ2VUeXBlX0NhcmRhbm9UeEhvc3RBY2sQvQIaBJC1GAESKAodTWVzc2FnZVR5cGVfQ2FyZGFub1R4Qm9keUhhc2gQvgIaBJi1GAESLAohTWVzc2FnZVR5cGVfQ2FyZGFub1NpZ25UeEZpbmlzaGVkEL8CGgSYtRgBEigKHU1lc3NhZ2VUeXBlX0NhcmRhbm9TaWduVHhJbml0EMACGgSQtRgBEiUKGk1lc3NhZ2VUeXBlX0NhcmRhbm9UeElucHV0EMECGgSQtRgBEiYKG01lc3NhZ2VUeXBlX0NhcmRhbm9UeE91dHB1dBDCAhoEkLUYARIoCh1NZXNzYWdlVHlwZV9DYXJkYW5vQXNzZXRHcm91cBDDAhoEkLUYARIjChhNZXNzYWdlVHlwZV9DYXJkYW5vVG9rZW4QxAIaBJC1GAESKwogTWVzc2FnZVR5cGVfQ2FyZGFub1R4Q2VydGlmaWNhdGUQxQIaBJC1GAESKgofTWVzc2FnZVR5cGVfQ2FyZGFub1R4V2l0aGRyYXdhbBDGAhoEkLUYARItCiJNZXNzYWdlVHlwZV9DYXJkYW5vVHhBdXhpbGlhcnlEYXRhEMcCGgSQtRgBEicKHE1lc3NhZ2VUeXBlX0NhcmRhbm9Qb29sT3duZXIQyAIaBJC1GAESMQomTWVzc2FnZVR5cGVfQ2FyZGFub1Bvb2xSZWxheVBhcmFtZXRlcnMQyQIaBJC1GAESMQomTWVzc2FnZVR5cGVfQ2FyZGFub0dldE5hdGl2ZVNjcmlwdEhhc2gQygIaBJC1GAESLgojTWVzc2FnZVR5cGVfQ2FyZGFub05hdGl2ZVNjcmlwdEhhc2gQywIaBJi1GAESJAoZTWVzc2FnZVR5cGVfQ2FyZGFub1R4TWludBDMAhoEkLUYARIvCiRNZXNzYWdlVHlwZV9DYXJkYW5vVHhDb2xsYXRlcmFsSW5wdXQQzQIaBJC1GAESLgojTWVzc2FnZVR5cGVfQ2FyZGFub1R4UmVxdWlyZWRTaWduZXIQzgIaBJC1GAESMAolTWVzc2FnZVR5cGVfQ2FyZGFub1R4SW5saW5lRGF0dW1DaHVuaxDPAhoEkLUYARI0CilNZXNzYWdlVHlwZV9DYXJkYW5vVHhSZWZlcmVuY2VTY3JpcHRDaHVuaxDQAhoEkLUYARIuCiNNZXNzYWdlVHlwZV9DYXJkYW5vVHhSZWZlcmVuY2VJbnB1dBDRAhoEkLUYARItCiJNZXNzYWdlVHlwZV9DYXJkYW5vU2lnbk1lc3NhZ2VJbml0ENICGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX0NhcmRhbm9NZXNzYWdlRGF0YVJlcXVlc3QQ0wIaBJi1GAESMQomTWVzc2FnZVR5cGVfQ2FyZGFub01lc3NhZ2VEYXRhUmVzcG9uc2UQ1AIaBJC1GAESLgojTWVzc2FnZVR5cGVfQ2FyZGFub01lc3NhZ2VTaWduYXR1cmUQ1QIaBJi1GAESJwocTWVzc2FnZVR5cGVfUmlwcGxlR2V0QWRkcmVzcxCQAxoEkLUYARIkChlNZXNzYWdlVHlwZV9SaXBwbGVBZGRyZXNzEJEDGgSYtRgBEiMKGE1lc3NhZ2VUeXBlX1JpcHBsZVNpZ25UeBCSAxoEkLUYARIlChpNZXNzYWdlVHlwZV9SaXBwbGVTaWduZWRUeBCTAxoEmLUYARIzCihNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbkluaXRSZXF1ZXN0EPUDGgSYtRgBEi8KJE1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uSW5pdEFjaxD2AxoEmLUYARI3CixNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvblNldElucHV0UmVxdWVzdBD3AxoEmLUYARIzCihNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvblNldElucHV0QWNrEPgDGgSYtRgBEjgKLU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uSW5wdXRWaW5pUmVxdWVzdBD7AxoEmLUYARI0CilNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbklucHV0VmluaUFjaxD8AxoEmLUYARI7CjBNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbkFsbElucHV0c1NldFJlcXVlc3QQ/QMaBJi1GAESNwosTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25BbGxJbnB1dHNTZXRBY2sQ/gMaBJi1GAESOAotTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25TZXRPdXRwdXRSZXF1ZXN0EP8DGgSYtRgBEjQKKU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uU2V0T3V0cHV0QWNrEIAEGgSYtRgBEjgKLU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uQWxsT3V0U2V0UmVxdWVzdBCBBBoEmLUYARI0CilNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbkFsbE91dFNldEFjaxCCBBoEmLUYARI4Ci1NZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvblNpZ25JbnB1dFJlcXVlc3QQgwQaBJi1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25TaWduSW5wdXRBY2sQhAQaBJi1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25GaW5hbFJlcXVlc3QQhQQaBJi1GAESMAolTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25GaW5hbEFjaxCGBBoEmLUYARI2CitNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZUV4cG9ydEluaXRSZXF1ZXN0EJIEGgSYtRgBEjIKJ01lc3NhZ2VUeXBlX01vbmVyb0tleUltYWdlRXhwb3J0SW5pdEFjaxCTBBoEmLUYARI0CilNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZVN5bmNTdGVwUmVxdWVzdBCUBBoEmLUYARIwCiVNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZVN5bmNTdGVwQWNrEJUEGgSYtRgBEjUKKk1lc3NhZ2VUeXBlX01vbmVyb0tleUltYWdlU3luY0ZpbmFsUmVxdWVzdBCWBBoEmLUYARIxCiZNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZVN5bmNGaW5hbEFjaxCXBBoEmLUYARInChxNZXNzYWdlVHlwZV9Nb25lcm9HZXRBZGRyZXNzEJwEGgSQtRgBEiQKGU1lc3NhZ2VUeXBlX01vbmVyb0FkZHJlc3MQnQQaBJi1GAESKAodTWVzc2FnZVR5cGVfTW9uZXJvR2V0V2F0Y2hLZXkQngQaBJC1GAESJQoaTWVzc2FnZVR5cGVfTW9uZXJvV2F0Y2hLZXkQnwQaBJi1GAESLQoiTWVzc2FnZVR5cGVfRGVidWdNb25lcm9EaWFnUmVxdWVzdBCiBBoEkLUYARIpCh5NZXNzYWdlVHlwZV9EZWJ1Z01vbmVyb0RpYWdBY2sQowQaBJi1GAESLAohTWVzc2FnZVR5cGVfTW9uZXJvR2V0VHhLZXlSZXF1ZXN0EKYEGgSQtRgBEigKHU1lc3NhZ2VUeXBlX01vbmVyb0dldFR4S2V5QWNrEKcEGgSYtRgBEjQKKU1lc3NhZ2VUeXBlX01vbmVyb0xpdmVSZWZyZXNoU3RhcnRSZXF1ZXN0EKgEGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX01vbmVyb0xpdmVSZWZyZXNoU3RhcnRBY2sQqQQaBJi1GAESMwooTWVzc2FnZVR5cGVfTW9uZXJvTGl2ZVJlZnJlc2hTdGVwUmVxdWVzdBCqBBoEkLUYARIvCiRNZXNzYWdlVHlwZV9Nb25lcm9MaXZlUmVmcmVzaFN0ZXBBY2sQqwQaBJi1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvTGl2ZVJlZnJlc2hGaW5hbFJlcXVlc3QQrAQaBJC1GAESMAolTWVzc2FnZVR5cGVfTW9uZXJvTGl2ZVJlZnJlc2hGaW5hbEFjaxCtBBoEmLUYARImChtNZXNzYWdlVHlwZV9Fb3NHZXRQdWJsaWNLZXkQ2AQaBJC1GAESIwoYTWVzc2FnZVR5cGVfRW9zUHVibGljS2V5ENkEGgSYtRgBEiAKFU1lc3NhZ2VUeXBlX0Vvc1NpZ25UeBDaBBoEkLUYARIpCh5NZXNzYWdlVHlwZV9Fb3NUeEFjdGlvblJlcXVlc3QQ2wQaBJi1GAESJQoaTWVzc2FnZVR5cGVfRW9zVHhBY3Rpb25BY2sQ3AQaBJC1GAESIgoXTWVzc2FnZVR5cGVfRW9zU2lnbmVkVHgQ3QQaBJi1GAESNgorTWVzc2FnZVR5cGVfV2ViQXV0aG5MaXN0UmVzaWRlbnRDcmVkZW50aWFscxCgBhoEkLUYARIqCh9NZXNzYWdlVHlwZV9XZWJBdXRobkNyZWRlbnRpYWxzEKEGGgSYtRgBEjQKKU1lc3NhZ2VUeXBlX1dlYkF1dGhuQWRkUmVzaWRlbnRDcmVkZW50aWFsEKIGGgSQtRgBEjcKLE1lc3NhZ2VUeXBlX1dlYkF1dGhuUmVtb3ZlUmVzaWRlbnRDcmVkZW50aWFsEKMGGgSQtRgBEi0KIk1lc3NhZ2VUeXBlX1dlYkF1dGhuQ3JlZGVudGlhbHNBY2sQpAYaBJC1GAESKQoeTWVzc2FnZVR5cGVfU29sYW5hR2V0UHVibGljS2V5EIQHGgSQtRgBEiYKG01lc3NhZ2VUeXBlX1NvbGFuYVB1YmxpY0tleRCFBxoEmLUYARInChxNZXNzYWdlVHlwZV9Tb2xhbmFHZXRBZGRyZXNzEIYHGgSQtRgBEiQKGU1lc3NhZ2VUeXBlX1NvbGFuYUFkZHJlc3MQhwcaBJi1GAESIwoYTWVzc2FnZVR5cGVfU29sYW5hU2lnblR4EIgHGgSQtRgBEigKHU1lc3NhZ2VUeXBlX1NvbGFuYVR4U2lnbmF0dXJlEIkHGgSYtRgBEi4KH01lc3NhZ2VUeXBlX1RocENyZWF0ZU5ld1Nlc3Npb24Q6AcaCJC1GAGAph0BEi8KIE1lc3NhZ2VUeXBlX1RocENyZWRlbnRpYWxSZXF1ZXN0EPgHGgiQtRgBgKYdARIwCiFNZXNzYWdlVHlwZV9UaHBDcmVkZW50aWFsUmVzcG9uc2UQ+QcaCJi1GAGAph0BEiUKGk1lc3NhZ2VUeXBlX05vc3RyR2V0UHVia2V5ENEPGgSQtRgBEiIKF01lc3NhZ2VUeXBlX05vc3RyUHVia2V5ENIPGgSYtRgBEiUKGk1lc3NhZ2VUeXBlX05vc3RyU2lnbkV2ZW50ENMPGgSQtRgBEioKH01lc3NhZ2VUeXBlX05vc3RyRXZlbnRTaWduYXR1cmUQ1A8aBJi1GAESJwoYTWVzc2FnZVR5cGVfRXZvbHVHZXROb2RlELQQGgiQtRgBgKYdARIkChVNZXNzYWdlVHlwZV9Fdm9sdU5vZGUQtRAaCJi1GAGAph0BEjcKKE1lc3NhZ2VUeXBlX0V2b2x1U2lnblJlZ2lzdHJhdGlvblJlcXVlc3QQthAaCJC1GAGAph0BEjMKJE1lc3NhZ2VUeXBlX0V2b2x1UmVnaXN0cmF0aW9uUmVxdWVzdBC3EBoImLUYAYCmHQESNwooTWVzc2FnZVR5cGVfRXZvbHVHZXREZWxlZ2F0ZWRJZGVudGl0eUtleRC4EBoIkLUYAYCmHQESNAolTWVzc2FnZVR5cGVfRXZvbHVEZWxlZ2F0ZWRJZGVudGl0eUtleRC5EBoImLUYAYCmHQESLwogTWVzc2FnZVR5cGVfRXZvbHVJbmRleE1hbmFnZW1lbnQQuhAaCJC1GAGAph0BEjcKKE1lc3NhZ2VUeXBlX0V2b2x1SW5kZXhNYW5hZ2VtZW50UmVzcG9uc2UQuxAaCJi1GAGAph0BEiUKGk1lc3NhZ2VUeXBlX1Ryb25HZXRBZGRyZXNzEJgRGgSQtRgBEiIKF01lc3NhZ2VUeXBlX1Ryb25BZGRyZXNzEJkRGgSYtRgBEiEKFk1lc3NhZ2VUeXBlX1Ryb25TaWduVHgQmhEaBJC1GAESJAoZTWVzc2FnZVR5cGVfVHJvblNpZ25hdHVyZRCbERoEmLUYARIqCh9NZXNzYWdlVHlwZV9Ucm9uQ29udHJhY3RSZXF1ZXN0EJwRGgSYtRgBEisKIE1lc3NhZ2VUeXBlX1Ryb25UcmFuc2ZlckNvbnRyYWN0EJ0RGgSQtRgBEi8KJE1lc3NhZ2VUeXBlX1Ryb25UcmlnZ2VyU21hcnRDb250cmFjdBCeERoEkLUYARIyCidNZXNzYWdlVHlwZV9Ucm9uRnJlZXplQmFsYW5jZVYyQ29udHJhY3QQnxEaBJC1GAESNAopTWVzc2FnZVR5cGVfVHJvblVuZnJlZXplQmFsYW5jZVYyQ29udHJhY3QQoBEaBJC1GAESKwogTWVzc2FnZVR5cGVfVHJvbldpdGhkcmF3VW5mcmVlemUQoREaBJC1GAESLgojTWVzc2FnZVR5cGVfVHJvblZvdGVXaXRuZXNzQ29udHJhY3QQohEaBJC1GAESKQoeTWVzc2FnZVR5cGVfQmVuY2htYXJrTGlzdE5hbWVzEIxHGgSAph0BEiUKGk1lc3NhZ2VUeXBlX0JlbmNobWFya05hbWVzEI1HGgSAph0BEiMKGE1lc3NhZ2VUeXBlX0JlbmNobWFya1J1bhCORxoEgKYdARImChtNZXNzYWdlVHlwZV9CZW5jaG1hcmtSZXN1bHQQj0caBICmHQESJwoYTWVzc2FnZVR5cGVfVGVsZW1ldHJ5R2V0EMwIGgiQtRgBgKYdARIkChVNZXNzYWdlVHlwZV9UZWxlbWV0cnkQzQgaCJi1GAGAph0BGgjI8xgB0PMYASIECFoQXCIECEcQSiIECHIQeiIFCHsQlQEiBgjbARDbASIGCOABEOABIgYIrAIQsAIiBgi1AhC4AiIGCLwFEMUFIgYI6QcQ9wciBgj6BxDLCEI4CiNjb20uc2F0b3NoaWxhYnMudHJlem9yLmxpYi5wcm90b2J1ZkINVHJlem9yTWVzc2FnZYCmHQE', - [file_options], -); -export const MessageTypeSchema = enumDesc(file_messages, 0); -export const MessageType = tsEnum(MessageTypeSchema); +/* eslint-disable */ + +import { enumDesc, fileDesc, tsEnum } from "@bufbuild/protobuf/codegenv2"; +import { file_options } from "./options_pb"; + +/** + * Describes the file messages.proto. + */ +export const file_messages = /*@__PURE__*/ + fileDesc("Cg5tZXNzYWdlcy5wcm90bxISaHcudHJlem9yLm1lc3NhZ2VzKtpoCgtNZXNzYWdlVHlwZRIoChZNZXNzYWdlVHlwZV9Jbml0aWFsaXplEAAaDICmHQGQtRgBsLUYARIeChBNZXNzYWdlVHlwZV9QaW5nEAEaCICmHQGQtRgBEiUKE01lc3NhZ2VUeXBlX1N1Y2Nlc3MQAhoMgKYdAZi1GAGotRgBEiUKE01lc3NhZ2VUeXBlX0ZhaWx1cmUQAxoMgKYdAZi1GAGotRgBEiMKFU1lc3NhZ2VUeXBlX0NoYW5nZVBpbhAEGgiAph0BkLUYARIkChZNZXNzYWdlVHlwZV9XaXBlRGV2aWNlEAUaCICmHQGQtRgBEiQKFk1lc3NhZ2VUeXBlX0dldEVudHJvcHkQCRoIgKYdAZC1GAESIQoTTWVzc2FnZVR5cGVfRW50cm9weRAKGgiAph0BmLUYARIkChZNZXNzYWdlVHlwZV9Mb2FkRGV2aWNlEA0aCICmHQGQtRgBEiUKF01lc3NhZ2VUeXBlX1Jlc2V0RGV2aWNlEA4aCICmHQGQtRgBEiEKE01lc3NhZ2VUeXBlX1NldEJ1c3kQEBoIgKYdAZC1GAESIgoUTWVzc2FnZVR5cGVfRmVhdHVyZXMQERoIgKYdAZi1GAESKgocTWVzc2FnZVR5cGVfUGluTWF0cml4UmVxdWVzdBASGgiAph0BmLUYARIuChhNZXNzYWdlVHlwZV9QaW5NYXRyaXhBY2sQExoQgKYdAZC1GAGwtRgBwLUYARIkChJNZXNzYWdlVHlwZV9DYW5jZWwQFBoMgKYdAZC1GAGwtRgBEiQKFk1lc3NhZ2VUeXBlX0xvY2tEZXZpY2UQGBoIgKYdAZC1GAESJwoZTWVzc2FnZVR5cGVfQXBwbHlTZXR0aW5ncxAZGgiAph0BkLUYARInChlNZXNzYWdlVHlwZV9CdXR0b25SZXF1ZXN0EBoaCICmHQGYtRgBEisKFU1lc3NhZ2VUeXBlX0J1dHRvbkFjaxAbGhCAph0BkLUYAbC1GAHAtRgBEiQKFk1lc3NhZ2VUeXBlX0FwcGx5RmxhZ3MQHBoIgKYdAZC1GAESIgoUTWVzc2FnZVR5cGVfR2V0Tm9uY2UQHxoIgKYdAZC1GAESHwoRTWVzc2FnZVR5cGVfTm9uY2UQIRoIgKYdAZi1GAESJgoYTWVzc2FnZVR5cGVfQmFja3VwRGV2aWNlECIaCICmHQGQtRgBEigKGk1lc3NhZ2VUeXBlX0VudHJvcHlSZXF1ZXN0ECMaCICmHQGYtRgBEiQKFk1lc3NhZ2VUeXBlX0VudHJvcHlBY2sQJBoIgKYdAZC1GAESKAoaTWVzc2FnZVR5cGVfUGF5bWVudFJlcXVlc3QQJRoIgKYdAZC1GAESLAodTWVzc2FnZVR5cGVfRW50cm9weUNoZWNrUmVhZHkQ4gcaCICmHQGYtRgBEi8KIE1lc3NhZ2VUeXBlX0VudHJvcHlDaGVja0NvbnRpbnVlEOMHGgiAph0BkLUYARIrCh1NZXNzYWdlVHlwZV9QYXNzcGhyYXNlUmVxdWVzdBApGgiAph0BmLUYARIvChlNZXNzYWdlVHlwZV9QYXNzcGhyYXNlQWNrECoaEICmHQGQtRgBsLUYAcC1GAESKAoaTWVzc2FnZVR5cGVfUmVjb3ZlcnlEZXZpY2UQLRoIgKYdAZC1GAESJQoXTWVzc2FnZVR5cGVfV29yZFJlcXVlc3QQLhoIgKYdAZi1GAESIQoTTWVzc2FnZVR5cGVfV29yZEFjaxAvGgiAph0BkLUYARIlChdNZXNzYWdlVHlwZV9HZXRGZWF0dXJlcxA3GgiAph0BkLUYARIjChVNZXNzYWdlVHlwZV9TZFByb3RlY3QQTxoIgKYdAZC1GAESKAoaTWVzc2FnZVR5cGVfQ2hhbmdlV2lwZUNvZGUQUhoIgKYdAZC1GAESJAoWTWVzc2FnZVR5cGVfRW5kU2Vzc2lvbhBTGgiAph0BkLUYARIpChtNZXNzYWdlVHlwZV9Eb1ByZWF1dGhvcml6ZWQQVBoIgKYdAZC1GAESLgogTWVzc2FnZVR5cGVfUHJlYXV0aG9yaXplZFJlcXVlc3QQVRoIgKYdAZi1GAESLQofTWVzc2FnZVR5cGVfQ2FuY2VsQXV0aG9yaXphdGlvbhBWGgiAph0BkLUYARIsCh5NZXNzYWdlVHlwZV9SZWJvb3RUb0Jvb3Rsb2FkZXIQVxoIgKYdAZC1GAESKQobTWVzc2FnZVR5cGVfR2V0RmlybXdhcmVIYXNoEFgaCICmHQGQtRgBEiYKGE1lc3NhZ2VUeXBlX0Zpcm13YXJlSGFzaBBZGgiAph0BmLUYARIkChZNZXNzYWdlVHlwZV9VbmxvY2tQYXRoEF0aCICmHQGQtRgBEi0KH01lc3NhZ2VUeXBlX1VubG9ja2VkUGF0aFJlcXVlc3QQXhoIgKYdAZi1GAESLAoeTWVzc2FnZVR5cGVfU2hvd0RldmljZVR1dG9yaWFsEF8aCICmHQGQtRgBEioKHE1lc3NhZ2VUeXBlX1VubG9ja0Jvb3Rsb2FkZXIQYBoIgKYdAZC1GAESLAoeTWVzc2FnZVR5cGVfQXV0aGVudGljYXRlRGV2aWNlEGEaCICmHQGQtRgBEisKHU1lc3NhZ2VUeXBlX0F1dGhlbnRpY2l0eVByb29mEGIaCICmHQGYtRgBEjAKIk1lc3NhZ2VUeXBlX0F1dGhlbnRpY2l0eVByb29mU2l6ZXMQYxoIgKYdAZi1GAESKQoaTWVzc2FnZVR5cGVfQ2hhbmdlTGFuZ3VhZ2UQ3gcaCICmHQGQtRgBEisKHE1lc3NhZ2VUeXBlX0RhdGFDaHVua1JlcXVlc3QQ3wcaCICmHQGYtRgBEicKGE1lc3NhZ2VUeXBlX0RhdGFDaHVua0FjaxDgBxoIgKYdAZC1GAESKAoZTWVzc2FnZVR5cGVfU2V0QnJpZ2h0bmVzcxDhBxoIgKYdAZC1GAESKgobTWVzc2FnZVR5cGVfR2V0U2VyaWFsTnVtYmVyEOQHGgiAph0BkLUYARInChhNZXNzYWdlVHlwZV9TZXJpYWxOdW1iZXIQ5QcaCICmHQGYtRgBEjQKJU1lc3NhZ2VUeXBlX0dldEF1dGhlbnRpY2l0eVByb29mQ2h1bmsQ5gcaCICmHQGQtRgBEjEKIk1lc3NhZ2VUeXBlX0F1dGhlbnRpY2l0eVByb29mQ2h1bmsQ5wcaCICmHQGYtRgBEiMKGU1lc3NhZ2VUeXBlX1NldFUyRkNvdW50ZXIQPxoEkLUYARInCh1NZXNzYWdlVHlwZV9HZXROZXh0VTJGQ291bnRlchBQGgSQtRgBEiQKGk1lc3NhZ2VUeXBlX05leHRVMkZDb3VudGVyEFEaBJi1GAESKwoZTWVzc2FnZVR5cGVfRmlybXdhcmVFcmFzZRAGGgyAph0BkLUYAbi1GAESLAoaTWVzc2FnZVR5cGVfRmlybXdhcmVVcGxvYWQQBxoMgKYdAZC1GAG4tRgBEi0KG01lc3NhZ2VUeXBlX0Zpcm13YXJlUmVxdWVzdBAIGgyAph0BmLUYAbi1GAESKAoWTWVzc2FnZVR5cGVfUHJvZFRlc3RUMRAgGgyAph0BkLUYAbi1GAESJAoVTWVzc2FnZVR5cGVfQmxlVW5wYWlyEME+GgiAph0BkLUYARImChhNZXNzYWdlVHlwZV9HZXRQdWJsaWNLZXkQCxoIgKYdAZC1GAESIwoVTWVzc2FnZVR5cGVfUHVibGljS2V5EAwaCICmHQGYtRgBEiAKEk1lc3NhZ2VUeXBlX1NpZ25UeBAPGgiAph0BkLUYARIjChVNZXNzYWdlVHlwZV9UeFJlcXVlc3QQFRoIgKYdAZi1GAESHwoRTWVzc2FnZVR5cGVfVHhBY2sQFhoIgKYdAZC1GAESJAoWTWVzc2FnZVR5cGVfR2V0QWRkcmVzcxAdGgiAph0BkLUYARIhChNNZXNzYWdlVHlwZV9BZGRyZXNzEB4aCICmHQGYtRgBEiUKF01lc3NhZ2VUeXBlX1NpZ25NZXNzYWdlECYaCICmHQGQtRgBEicKGU1lc3NhZ2VUeXBlX1ZlcmlmeU1lc3NhZ2UQJxoIgKYdAZC1GAESKgocTWVzc2FnZVR5cGVfTWVzc2FnZVNpZ25hdHVyZRAoGgiAph0BmLUYARIoChpNZXNzYWdlVHlwZV9HZXRPd25lcnNoaXBJZBArGgiAph0BkLUYARIlChdNZXNzYWdlVHlwZV9Pd25lcnNoaXBJZBAsGgiAph0BmLUYARIrCh1NZXNzYWdlVHlwZV9HZXRPd25lcnNoaXBQcm9vZhAxGgiAph0BkLUYARIoChpNZXNzYWdlVHlwZV9Pd25lcnNoaXBQcm9vZhAyGgiAph0BmLUYARIrCh1NZXNzYWdlVHlwZV9BdXRob3JpemVDb2luSm9pbhAzGgiAph0BkLUYARIoChpNZXNzYWdlVHlwZV9DaXBoZXJLZXlWYWx1ZRAXGgiAph0BkLUYARIqChxNZXNzYWdlVHlwZV9DaXBoZXJlZEtleVZhbHVlEDAaCICmHQGYtRgBEiYKGE1lc3NhZ2VUeXBlX1NpZ25JZGVudGl0eRA1GgiAph0BkLUYARIoChpNZXNzYWdlVHlwZV9TaWduZWRJZGVudGl0eRA2GgiAph0BmLUYARIrCh1NZXNzYWdlVHlwZV9HZXRFQ0RIU2Vzc2lvbktleRA9GgiAph0BkLUYARIoChpNZXNzYWdlVHlwZV9FQ0RIU2Vzc2lvbktleRA+GgiAph0BmLUYARItCh9NZXNzYWdlVHlwZV9QYXltZW50Tm90aWZpY2F0aW9uEDQaCICmHQGQtRgBEjMKHU1lc3NhZ2VUeXBlX0RlYnVnTGlua0RlY2lzaW9uEGQaEICmHQGgtRgBsLUYAcC1GAESLwodTWVzc2FnZVR5cGVfRGVidWdMaW5rR2V0U3RhdGUQZRoMgKYdAaC1GAGwtRgBEigKGk1lc3NhZ2VUeXBlX0RlYnVnTGlua1N0YXRlEGYaCICmHQGotRgBEicKGU1lc3NhZ2VUeXBlX0RlYnVnTGlua1N0b3AQZxoIgKYdAaC1GAESJgoYTWVzc2FnZVR5cGVfRGVidWdMaW5rTG9nEGgaCICmHQGotRgBEi0KH01lc3NhZ2VUeXBlX0RlYnVnTGlua01lbW9yeVJlYWQQbhoIgKYdAaC1GAESKQobTWVzc2FnZVR5cGVfRGVidWdMaW5rTWVtb3J5EG8aCICmHQGotRgBEi4KIE1lc3NhZ2VUeXBlX0RlYnVnTGlua01lbW9yeVdyaXRlEHAaCICmHQGgtRgBEi0KH01lc3NhZ2VUeXBlX0RlYnVnTGlua0ZsYXNoRXJhc2UQcRoIgKYdAaC1GAESKgobTWVzc2FnZVR5cGVfRGVidWdMaW5rTGF5b3V0EKlGGgiAph0BqLUYARIwCiFNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtSZXNlZWRSYW5kb20QqkYaCICmHQGgtRgBEjAKIU1lc3NhZ2VUeXBlX0RlYnVnTGlua1JlY29yZFNjcmVlbhCrRhoIgKYdAaC1GAESLwogTWVzc2FnZVR5cGVfRGVidWdMaW5rRXJhc2VTZENhcmQQrUYaCICmHQGgtRgBEi8KIE1lc3NhZ2VUeXBlX0RlYnVnTGlua1dhdGNoTGF5b3V0EK5GGgiAph0BoLUYARI0CiVNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtSZXNldERlYnVnRXZlbnRzEK9GGgiAph0BoLUYARIzCiRNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtPcHRpZ2FTZXRTZWNNYXgQsEYaCICmHQGgtRgBEi0KHk1lc3NhZ2VUeXBlX0RlYnVnTGlua0dldEdjSW5mbxCxRhoIgKYdAaC1GAESKgobTWVzc2FnZVR5cGVfRGVidWdMaW5rR2NJbmZvELJGGgiAph0BqLUYARIyCiNNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtHZXRQYWlyaW5nSW5mbxCzRhoIgKYdAaC1GAESLwogTWVzc2FnZVR5cGVfRGVidWdMaW5rUGFpcmluZ0luZm8QtEYaCICmHQGotRgBEjAKIU1lc3NhZ2VUeXBlX0RlYnVnTGlua1NldExvZ0ZpbHRlchC1RhoIgKYdAaC1GAESMQoiTWVzc2FnZVR5cGVfRGVidWdMaW5rTjRXMUNvbm5lY3RlZBC2RhoIgKYdAaC1GAESLQoeTWVzc2FnZVR5cGVfRGVidWdMaW5rTjRXMVdyaXRlELdGGgiAph0BqLUYARIsCh1NZXNzYWdlVHlwZV9EZWJ1Z0xpbmtONFcxUmVhZBC4RhoIgKYdAai1GAESMAohTWVzc2FnZVR5cGVfRGVidWdMaW5rTjRXMVJlc3BvbnNlELlGGgiAph0BoLUYARIzCiRNZXNzYWdlVHlwZV9EZWJ1Z0xpbmtTZXRCYXR0ZXJ5U3RhdGUQukYaCICmHQGgtRgBEisKIE1lc3NhZ2VUeXBlX0V0aGVyZXVtR2V0UHVibGljS2V5EMIDGgSQtRgBEigKHU1lc3NhZ2VUeXBlX0V0aGVyZXVtUHVibGljS2V5EMMDGgSYtRgBEigKHk1lc3NhZ2VUeXBlX0V0aGVyZXVtR2V0QWRkcmVzcxA4GgSQtRgBEiUKG01lc3NhZ2VUeXBlX0V0aGVyZXVtQWRkcmVzcxA5GgSYtRgBEiQKGk1lc3NhZ2VUeXBlX0V0aGVyZXVtU2lnblR4EDoaBJC1GAESLAohTWVzc2FnZVR5cGVfRXRoZXJldW1TaWduVHhFSVAxNTU5EMQDGgSQtRgBEicKHU1lc3NhZ2VUeXBlX0V0aGVyZXVtVHhSZXF1ZXN0EDsaBJi1GAESIwoZTWVzc2FnZVR5cGVfRXRoZXJldW1UeEFjaxA8GgSQtRgBEikKH01lc3NhZ2VUeXBlX0V0aGVyZXVtU2lnbk1lc3NhZ2UQQBoEkLUYARIrCiFNZXNzYWdlVHlwZV9FdGhlcmV1bVZlcmlmeU1lc3NhZ2UQQRoEkLUYARIuCiRNZXNzYWdlVHlwZV9FdGhlcmV1bU1lc3NhZ2VTaWduYXR1cmUQQhoEmLUYARIsCiFNZXNzYWdlVHlwZV9FdGhlcmV1bVNpZ25UeXBlZERhdGEQ0AMaBJC1GAESNQoqTWVzc2FnZVR5cGVfRXRoZXJldW1UeXBlZERhdGFTdHJ1Y3RSZXF1ZXN0ENEDGgSYtRgBEjEKJk1lc3NhZ2VUeXBlX0V0aGVyZXVtVHlwZWREYXRhU3RydWN0QWNrENIDGgSQtRgBEjQKKU1lc3NhZ2VUeXBlX0V0aGVyZXVtVHlwZWREYXRhVmFsdWVSZXF1ZXN0ENMDGgSYtRgBEjAKJU1lc3NhZ2VUeXBlX0V0aGVyZXVtVHlwZWREYXRhVmFsdWVBY2sQ1AMaBJC1GAESMQomTWVzc2FnZVR5cGVfRXRoZXJldW1UeXBlZERhdGFTaWduYXR1cmUQ1QMaBJi1GAESLAohTWVzc2FnZVR5cGVfRXRoZXJldW1TaWduVHlwZWRIYXNoENYDGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX0V0aGVyZXVtRGVmaW5pdGlvblJlcXVlc3QQ1wMaBJi1GAESLAohTWVzc2FnZVR5cGVfRXRoZXJldW1EZWZpbml0aW9uQWNrENgDGgSQtRgBEisKIE1lc3NhZ2VUeXBlX0V0aGVyZXVtU2lnbkF1dGg3NzAyENkDGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX0V0aGVyZXVtQXV0aDc3MDJTaWduYXR1cmUQ2gMaBJi1GAESIwoZTWVzc2FnZVR5cGVfTkVNR2V0QWRkcmVzcxBDGgSQtRgBEiAKFk1lc3NhZ2VUeXBlX05FTUFkZHJlc3MQRBoEmLUYARIfChVNZXNzYWdlVHlwZV9ORU1TaWduVHgQRRoEkLUYARIhChdNZXNzYWdlVHlwZV9ORU1TaWduZWRUeBBGGgSYtRgBEicKHU1lc3NhZ2VUeXBlX05FTURlY3J5cHRNZXNzYWdlEEsaBJC1GAESKQofTWVzc2FnZVR5cGVfTkVNRGVjcnlwdGVkTWVzc2FnZRBMGgSYtRgBEiYKG01lc3NhZ2VUeXBlX1Rlem9zR2V0QWRkcmVzcxCWARoEkLUYARIjChhNZXNzYWdlVHlwZV9UZXpvc0FkZHJlc3MQlwEaBJi1GAESIgoXTWVzc2FnZVR5cGVfVGV6b3NTaWduVHgQmAEaBJC1GAESJAoZTWVzc2FnZVR5cGVfVGV6b3NTaWduZWRUeBCZARoEmLUYARIoCh1NZXNzYWdlVHlwZV9UZXpvc0dldFB1YmxpY0tleRCaARoEkLUYARIlChpNZXNzYWdlVHlwZV9UZXpvc1B1YmxpY0tleRCbARoEmLUYARIkChlNZXNzYWdlVHlwZV9TdGVsbGFyU2lnblR4EMoBGgSQtRgBEikKHk1lc3NhZ2VUeXBlX1N0ZWxsYXJUeE9wUmVxdWVzdBDLARoEmLUYARIoCh1NZXNzYWdlVHlwZV9TdGVsbGFyR2V0QWRkcmVzcxDPARoEkLUYARIlChpNZXNzYWdlVHlwZV9TdGVsbGFyQWRkcmVzcxDQARoEmLUYARItCiJNZXNzYWdlVHlwZV9TdGVsbGFyQ3JlYXRlQWNjb3VudE9wENIBGgSQtRgBEicKHE1lc3NhZ2VUeXBlX1N0ZWxsYXJQYXltZW50T3AQ0wEaBJC1GAESOAotTWVzc2FnZVR5cGVfU3RlbGxhclBhdGhQYXltZW50U3RyaWN0UmVjZWl2ZU9wENQBGgSQtRgBEi8KJE1lc3NhZ2VUeXBlX1N0ZWxsYXJNYW5hZ2VTZWxsT2ZmZXJPcBDVARoEkLUYARI2CitNZXNzYWdlVHlwZV9TdGVsbGFyQ3JlYXRlUGFzc2l2ZVNlbGxPZmZlck9wENYBGgSQtRgBEioKH01lc3NhZ2VUeXBlX1N0ZWxsYXJTZXRPcHRpb25zT3AQ1wEaBJC1GAESKwogTWVzc2FnZVR5cGVfU3RlbGxhckNoYW5nZVRydXN0T3AQ2AEaBJC1GAESKgofTWVzc2FnZVR5cGVfU3RlbGxhckFsbG93VHJ1c3RPcBDZARoEkLUYARIsCiFNZXNzYWdlVHlwZV9TdGVsbGFyQWNjb3VudE1lcmdlT3AQ2gEaBJC1GAESKgofTWVzc2FnZVR5cGVfU3RlbGxhck1hbmFnZURhdGFPcBDcARoEkLUYARIsCiFNZXNzYWdlVHlwZV9TdGVsbGFyQnVtcFNlcXVlbmNlT3AQ3QEaBJC1GAESLgojTWVzc2FnZVR5cGVfU3RlbGxhck1hbmFnZUJ1eU9mZmVyT3AQ3gEaBJC1GAESNQoqTWVzc2FnZVR5cGVfU3RlbGxhclBhdGhQYXltZW50U3RyaWN0U2VuZE9wEN8BGgSQtRgBEjUKKk1lc3NhZ2VUeXBlX1N0ZWxsYXJDbGFpbUNsYWltYWJsZUJhbGFuY2VPcBDhARoEkLUYARImChtNZXNzYWdlVHlwZV9TdGVsbGFyU2lnbmVkVHgQ5gEaBJi1GAESMgonTWVzc2FnZVR5cGVfU3RlbGxhckludm9rZUhvc3RGdW5jdGlvbk9wEOsBGgSQtRgBEioKH01lc3NhZ2VUeXBlX1N0ZWxsYXJUeEV4dFJlcXVlc3QQ7gEaBJi1GAESIwoYTWVzc2FnZVR5cGVfU3RlbGxhclR4RXh0EO8BGgSQtRgBEioKH01lc3NhZ2VUeXBlX0NhcmRhbm9HZXRQdWJsaWNLZXkQsQIaBJC1GAESJwocTWVzc2FnZVR5cGVfQ2FyZGFub1B1YmxpY0tleRCyAhoEmLUYARIoCh1NZXNzYWdlVHlwZV9DYXJkYW5vR2V0QWRkcmVzcxCzAhoEkLUYARIlChpNZXNzYWdlVHlwZV9DYXJkYW5vQWRkcmVzcxC0AhoEmLUYARInChxNZXNzYWdlVHlwZV9DYXJkYW5vVHhJdGVtQWNrELkCGgSYtRgBEjcKLE1lc3NhZ2VUeXBlX0NhcmRhbm9UeEF1eGlsaWFyeURhdGFTdXBwbGVtZW50ELoCGgSYtRgBEi4KI01lc3NhZ2VUeXBlX0NhcmRhbm9UeFdpdG5lc3NSZXF1ZXN0ELsCGgSQtRgBEi8KJE1lc3NhZ2VUeXBlX0NhcmRhbm9UeFdpdG5lc3NSZXNwb25zZRC8AhoEmLUYARInChxNZXNzYWdlVHlwZV9DYXJkYW5vVHhIb3N0QWNrEL0CGgSQtRgBEigKHU1lc3NhZ2VUeXBlX0NhcmRhbm9UeEJvZHlIYXNoEL4CGgSYtRgBEiwKIU1lc3NhZ2VUeXBlX0NhcmRhbm9TaWduVHhGaW5pc2hlZBC/AhoEmLUYARIoCh1NZXNzYWdlVHlwZV9DYXJkYW5vU2lnblR4SW5pdBDAAhoEkLUYARIlChpNZXNzYWdlVHlwZV9DYXJkYW5vVHhJbnB1dBDBAhoEkLUYARImChtNZXNzYWdlVHlwZV9DYXJkYW5vVHhPdXRwdXQQwgIaBJC1GAESKAodTWVzc2FnZVR5cGVfQ2FyZGFub0Fzc2V0R3JvdXAQwwIaBJC1GAESIwoYTWVzc2FnZVR5cGVfQ2FyZGFub1Rva2VuEMQCGgSQtRgBEisKIE1lc3NhZ2VUeXBlX0NhcmRhbm9UeENlcnRpZmljYXRlEMUCGgSQtRgBEioKH01lc3NhZ2VUeXBlX0NhcmRhbm9UeFdpdGhkcmF3YWwQxgIaBJC1GAESLQoiTWVzc2FnZVR5cGVfQ2FyZGFub1R4QXV4aWxpYXJ5RGF0YRDHAhoEkLUYARInChxNZXNzYWdlVHlwZV9DYXJkYW5vUG9vbE93bmVyEMgCGgSQtRgBEjEKJk1lc3NhZ2VUeXBlX0NhcmRhbm9Qb29sUmVsYXlQYXJhbWV0ZXJzEMkCGgSQtRgBEjEKJk1lc3NhZ2VUeXBlX0NhcmRhbm9HZXROYXRpdmVTY3JpcHRIYXNoEMoCGgSQtRgBEi4KI01lc3NhZ2VUeXBlX0NhcmRhbm9OYXRpdmVTY3JpcHRIYXNoEMsCGgSYtRgBEiQKGU1lc3NhZ2VUeXBlX0NhcmRhbm9UeE1pbnQQzAIaBJC1GAESLwokTWVzc2FnZVR5cGVfQ2FyZGFub1R4Q29sbGF0ZXJhbElucHV0EM0CGgSQtRgBEi4KI01lc3NhZ2VUeXBlX0NhcmRhbm9UeFJlcXVpcmVkU2lnbmVyEM4CGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX0NhcmRhbm9UeElubGluZURhdHVtQ2h1bmsQzwIaBJC1GAESNAopTWVzc2FnZVR5cGVfQ2FyZGFub1R4UmVmZXJlbmNlU2NyaXB0Q2h1bmsQ0AIaBJC1GAESLgojTWVzc2FnZVR5cGVfQ2FyZGFub1R4UmVmZXJlbmNlSW5wdXQQ0QIaBJC1GAESLQoiTWVzc2FnZVR5cGVfQ2FyZGFub1NpZ25NZXNzYWdlSW5pdBDSAhoEkLUYARIwCiVNZXNzYWdlVHlwZV9DYXJkYW5vTWVzc2FnZURhdGFSZXF1ZXN0ENMCGgSYtRgBEjEKJk1lc3NhZ2VUeXBlX0NhcmRhbm9NZXNzYWdlRGF0YVJlc3BvbnNlENQCGgSQtRgBEi4KI01lc3NhZ2VUeXBlX0NhcmRhbm9NZXNzYWdlU2lnbmF0dXJlENUCGgSYtRgBEicKHE1lc3NhZ2VUeXBlX1JpcHBsZUdldEFkZHJlc3MQkAMaBJC1GAESJAoZTWVzc2FnZVR5cGVfUmlwcGxlQWRkcmVzcxCRAxoEmLUYARIjChhNZXNzYWdlVHlwZV9SaXBwbGVTaWduVHgQkgMaBJC1GAESJQoaTWVzc2FnZVR5cGVfUmlwcGxlU2lnbmVkVHgQkwMaBJi1GAESMwooTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25Jbml0UmVxdWVzdBD1AxoEkLUYARIvCiRNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbkluaXRBY2sQ9gMaBJi1GAESNwosTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25TZXRJbnB1dFJlcXVlc3QQ9wMaBJC1GAESMwooTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25TZXRJbnB1dEFjaxD4AxoEmLUYARI4Ci1NZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbklucHV0VmluaVJlcXVlc3QQ+wMaBJC1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25JbnB1dFZpbmlBY2sQ/AMaBJi1GAESOwowTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25BbGxJbnB1dHNTZXRSZXF1ZXN0EP0DGgSQtRgBEjcKLE1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uQWxsSW5wdXRzU2V0QWNrEP4DGgSYtRgBEjgKLU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uU2V0T3V0cHV0UmVxdWVzdBD/AxoEkLUYARI0CilNZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvblNldE91dHB1dEFjaxCABBoEmLUYARI4Ci1NZXNzYWdlVHlwZV9Nb25lcm9UcmFuc2FjdGlvbkFsbE91dFNldFJlcXVlc3QQgQQaBJC1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25BbGxPdXRTZXRBY2sQggQaBJi1GAESOAotTWVzc2FnZVR5cGVfTW9uZXJvVHJhbnNhY3Rpb25TaWduSW5wdXRSZXF1ZXN0EIMEGgSQtRgBEjQKKU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uU2lnbklucHV0QWNrEIQEGgSYtRgBEjQKKU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uRmluYWxSZXF1ZXN0EIUEGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX01vbmVyb1RyYW5zYWN0aW9uRmluYWxBY2sQhgQaBJi1GAESNgorTWVzc2FnZVR5cGVfTW9uZXJvS2V5SW1hZ2VFeHBvcnRJbml0UmVxdWVzdBCSBBoEkLUYARIyCidNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZUV4cG9ydEluaXRBY2sQkwQaBJi1GAESNAopTWVzc2FnZVR5cGVfTW9uZXJvS2V5SW1hZ2VTeW5jU3RlcFJlcXVlc3QQlAQaBJC1GAESMAolTWVzc2FnZVR5cGVfTW9uZXJvS2V5SW1hZ2VTeW5jU3RlcEFjaxCVBBoEmLUYARI1CipNZXNzYWdlVHlwZV9Nb25lcm9LZXlJbWFnZVN5bmNGaW5hbFJlcXVlc3QQlgQaBJC1GAESMQomTWVzc2FnZVR5cGVfTW9uZXJvS2V5SW1hZ2VTeW5jRmluYWxBY2sQlwQaBJi1GAESJwocTWVzc2FnZVR5cGVfTW9uZXJvR2V0QWRkcmVzcxCcBBoEkLUYARIkChlNZXNzYWdlVHlwZV9Nb25lcm9BZGRyZXNzEJ0EGgSYtRgBEigKHU1lc3NhZ2VUeXBlX01vbmVyb0dldFdhdGNoS2V5EJ4EGgSQtRgBEiUKGk1lc3NhZ2VUeXBlX01vbmVyb1dhdGNoS2V5EJ8EGgSYtRgBEi0KIk1lc3NhZ2VUeXBlX0RlYnVnTW9uZXJvRGlhZ1JlcXVlc3QQogQaBJC1GAESKQoeTWVzc2FnZVR5cGVfRGVidWdNb25lcm9EaWFnQWNrEKMEGgSYtRgBEiwKIU1lc3NhZ2VUeXBlX01vbmVyb0dldFR4S2V5UmVxdWVzdBCmBBoEkLUYARIoCh1NZXNzYWdlVHlwZV9Nb25lcm9HZXRUeEtleUFjaxCnBBoEmLUYARI0CilNZXNzYWdlVHlwZV9Nb25lcm9MaXZlUmVmcmVzaFN0YXJ0UmVxdWVzdBCoBBoEkLUYARIwCiVNZXNzYWdlVHlwZV9Nb25lcm9MaXZlUmVmcmVzaFN0YXJ0QWNrEKkEGgSYtRgBEjMKKE1lc3NhZ2VUeXBlX01vbmVyb0xpdmVSZWZyZXNoU3RlcFJlcXVlc3QQqgQaBJC1GAESLwokTWVzc2FnZVR5cGVfTW9uZXJvTGl2ZVJlZnJlc2hTdGVwQWNrEKsEGgSYtRgBEjQKKU1lc3NhZ2VUeXBlX01vbmVyb0xpdmVSZWZyZXNoRmluYWxSZXF1ZXN0EKwEGgSQtRgBEjAKJU1lc3NhZ2VUeXBlX01vbmVyb0xpdmVSZWZyZXNoRmluYWxBY2sQrQQaBJi1GAESJgobTWVzc2FnZVR5cGVfRW9zR2V0UHVibGljS2V5ENgEGgSQtRgBEiMKGE1lc3NhZ2VUeXBlX0Vvc1B1YmxpY0tleRDZBBoEmLUYARIgChVNZXNzYWdlVHlwZV9Fb3NTaWduVHgQ2gQaBJC1GAESKQoeTWVzc2FnZVR5cGVfRW9zVHhBY3Rpb25SZXF1ZXN0ENsEGgSYtRgBEiUKGk1lc3NhZ2VUeXBlX0Vvc1R4QWN0aW9uQWNrENwEGgSQtRgBEiIKF01lc3NhZ2VUeXBlX0Vvc1NpZ25lZFR4EN0EGgSYtRgBEjYKK01lc3NhZ2VUeXBlX1dlYkF1dGhuTGlzdFJlc2lkZW50Q3JlZGVudGlhbHMQoAYaBJC1GAESKgofTWVzc2FnZVR5cGVfV2ViQXV0aG5DcmVkZW50aWFscxChBhoEmLUYARI0CilNZXNzYWdlVHlwZV9XZWJBdXRobkFkZFJlc2lkZW50Q3JlZGVudGlhbBCiBhoEkLUYARI3CixNZXNzYWdlVHlwZV9XZWJBdXRoblJlbW92ZVJlc2lkZW50Q3JlZGVudGlhbBCjBhoEkLUYARItCiJNZXNzYWdlVHlwZV9XZWJBdXRobkNyZWRlbnRpYWxzQWNrEKQGGgSQtRgBEikKHk1lc3NhZ2VUeXBlX1NvbGFuYUdldFB1YmxpY0tleRCEBxoEkLUYARImChtNZXNzYWdlVHlwZV9Tb2xhbmFQdWJsaWNLZXkQhQcaBJi1GAESJwocTWVzc2FnZVR5cGVfU29sYW5hR2V0QWRkcmVzcxCGBxoEkLUYARIkChlNZXNzYWdlVHlwZV9Tb2xhbmFBZGRyZXNzEIcHGgSYtRgBEiMKGE1lc3NhZ2VUeXBlX1NvbGFuYVNpZ25UeBCIBxoEkLUYARIoCh1NZXNzYWdlVHlwZV9Tb2xhbmFUeFNpZ25hdHVyZRCJBxoEmLUYARIoCh1NZXNzYWdlVHlwZV9Tb2xhbmFTaWduTWVzc2FnZRCKBxoEkLUYARItCiJNZXNzYWdlVHlwZV9Tb2xhbmFNZXNzYWdlU2lnbmF0dXJlEIsHGgSYtRgBEioKH01lc3NhZ2VUeXBlX1NvbGFuYVZlcmlmeU1lc3NhZ2UQjAcaBJC1GAESLgofTWVzc2FnZVR5cGVfVGhwQ3JlYXRlTmV3U2Vzc2lvbhDoBxoIkLUYAYCmHQESLwogTWVzc2FnZVR5cGVfVGhwQ3JlZGVudGlhbFJlcXVlc3QQ+AcaCJC1GAGAph0BEjAKIU1lc3NhZ2VUeXBlX1RocENyZWRlbnRpYWxSZXNwb25zZRD5BxoImLUYAYCmHQESJQoaTWVzc2FnZVR5cGVfTm9zdHJHZXRQdWJrZXkQ0Q8aBJC1GAESIgoXTWVzc2FnZVR5cGVfTm9zdHJQdWJrZXkQ0g8aBJi1GAESJQoaTWVzc2FnZVR5cGVfTm9zdHJTaWduRXZlbnQQ0w8aBJC1GAESKgofTWVzc2FnZVR5cGVfTm9zdHJFdmVudFNpZ25hdHVyZRDUDxoEmLUYARInChhNZXNzYWdlVHlwZV9Fdm9sdUdldE5vZGUQtBAaCICmHQGQtRgBEiQKFU1lc3NhZ2VUeXBlX0V2b2x1Tm9kZRC1EBoIgKYdAZi1GAESNwooTWVzc2FnZVR5cGVfRXZvbHVTaWduUmVnaXN0cmF0aW9uUmVxdWVzdBC2EBoIgKYdAZC1GAESMwokTWVzc2FnZVR5cGVfRXZvbHVSZWdpc3RyYXRpb25SZXF1ZXN0ELcQGgiAph0BmLUYARI3CihNZXNzYWdlVHlwZV9Fdm9sdUdldERlbGVnYXRlZElkZW50aXR5S2V5ELgQGgiAph0BkLUYARI0CiVNZXNzYWdlVHlwZV9Fdm9sdURlbGVnYXRlZElkZW50aXR5S2V5ELkQGgiAph0BmLUYARIvCiBNZXNzYWdlVHlwZV9Fdm9sdUluZGV4TWFuYWdlbWVudBC6EBoIgKYdAZC1GAESNwooTWVzc2FnZVR5cGVfRXZvbHVJbmRleE1hbmFnZW1lbnRSZXNwb25zZRC7EBoIgKYdAZi1GAESJQoaTWVzc2FnZVR5cGVfVHJvbkdldEFkZHJlc3MQmBEaBJC1GAESIgoXTWVzc2FnZVR5cGVfVHJvbkFkZHJlc3MQmREaBJi1GAESIQoWTWVzc2FnZVR5cGVfVHJvblNpZ25UeBCaERoEkLUYARIkChlNZXNzYWdlVHlwZV9Ucm9uU2lnbmF0dXJlEJsRGgSYtRgBEioKH01lc3NhZ2VUeXBlX1Ryb25Db250cmFjdFJlcXVlc3QQnBEaBJi1GAESKwogTWVzc2FnZVR5cGVfVHJvblRyYW5zZmVyQ29udHJhY3QQnREaBJC1GAESLwokTWVzc2FnZVR5cGVfVHJvblRyaWdnZXJTbWFydENvbnRyYWN0EJ4RGgSQtRgBEjIKJ01lc3NhZ2VUeXBlX1Ryb25GcmVlemVCYWxhbmNlVjJDb250cmFjdBCfERoEkLUYARI0CilNZXNzYWdlVHlwZV9Ucm9uVW5mcmVlemVCYWxhbmNlVjJDb250cmFjdBCgERoEkLUYARIrCiBNZXNzYWdlVHlwZV9Ucm9uV2l0aGRyYXdVbmZyZWV6ZRChERoEkLUYARIuCiNNZXNzYWdlVHlwZV9Ucm9uVm90ZVdpdG5lc3NDb250cmFjdBCiERoEkLUYARIqCh9NZXNzYWdlVHlwZV9Ucm9uV2l0aGRyYXdCYWxhbmNlEKURGgSQtRgBEikKHk1lc3NhZ2VUeXBlX0JlbmNobWFya0xpc3ROYW1lcxCMRxoEgKYdARIlChpNZXNzYWdlVHlwZV9CZW5jaG1hcmtOYW1lcxCNRxoEgKYdARIjChhNZXNzYWdlVHlwZV9CZW5jaG1hcmtSdW4QjkcaBICmHQESJgobTWVzc2FnZVR5cGVfQmVuY2htYXJrUmVzdWx0EI9HGgSAph0BEicKGE1lc3NhZ2VUeXBlX1RlbGVtZXRyeUdldBDMCBoIgKYdAZC1GAESJAoVTWVzc2FnZVR5cGVfVGVsZW1ldHJ5EM0IGgiAph0BmLUYARoIyPMYAdDzGAEiBAhaEFwiBAhNEE4iBAhHEEoiBAhyEHoiBQh7EJUBIgYI2wEQ2wEiBgjgARDgASIGCOIBEOIBIgYI4wEQ4wEiBgjkARDkASIGCOUBEOUBIgYI5wEQ5wEiBgjoARDoASIGCOkBEOkBIgYI6gEQ6gEiBgjsARDsASIGCO0BEO0BIgYIrAIQsAIiBgi1AhC4AiIGCLwFEMUFIgYI6QcQ9wciBgj6BxDLCEI4CiNjb20uc2F0b3NoaWxhYnMudHJlem9yLmxpYi5wcm90b2J1ZkINVHJlem9yTWVzc2FnZYCmHQE", [file_options]); + +/** + * Describes the enum hw.trezor.messages.MessageType. + */ +export const MessageTypeSchema = /*@__PURE__*/ + enumDesc(file_messages, 0); + +/** + * * + * Mapping between Trezor wire identifier (uint) and a protobuf message + * + * @generated from enum hw.trezor.messages.MessageType + */ +export const MessageType = /*@__PURE__*/ + tsEnum(MessageTypeSchema); diff --git a/packages/hwk-trezor-protobuf/src/manager.test.ts b/packages/hwk-trezor-protobuf/src/manager.test.ts index c74c4be05..83807baa2 100644 --- a/packages/hwk-trezor-protobuf/src/manager.test.ts +++ b/packages/hwk-trezor-protobuf/src/manager.test.ts @@ -19,10 +19,53 @@ describe('protobufManager logging', () => { expect(logSpy).not.toHaveBeenCalledWith( expect.stringContaining('[TREZOR_VERIFY]'), - expect.anything(), + expect.anything() ); } finally { logSpy.mockRestore(); } }); + + it('encodes and decodes the current streamed Trezor authenticity messages', () => { + const request = protobufManager.encode('AuthenticateDevice', { + challenge: 'ab'.repeat(32), + stream: true, + }); + expect(protobufManager.decode('AuthenticateDevice', request.message).message).toEqual({ + challenge: 'ab'.repeat(32), + stream: true, + }); + + const sizes = protobufManager.encode('AuthenticityProofSizes', { + optiga_certificates: [420], + optiga_signature: 72, + tropic_certificates: [410], + tropic_signature: 64, + mcu_certificates: [4067], + mcu_signature: 2420, + }); + expect(protobufManager.decode('AuthenticityProofSizes', sizes.message).message).toEqual({ + optiga_certificates: [420], + optiga_signature: 72, + tropic_certificates: [410], + tropic_signature: 64, + mcu_certificates: [4067], + mcu_signature: 2420, + }); + + const chunkRequest = protobufManager.encode('GetAuthenticityProofChunk', { + proof_type: 2, + index: 0, + offset: 500, + size: 500, + }); + expect( + protobufManager.decode('GetAuthenticityProofChunk', chunkRequest.message).message + ).toEqual({ + proof_type: 'MCU', + index: 0, + offset: 500, + size: 500, + }); + }); }); diff --git a/packages/hwk-trezor-protobuf/src/manager.ts b/packages/hwk-trezor-protobuf/src/manager.ts index e8b4ea835..3479bddb9 100644 --- a/packages/hwk-trezor-protobuf/src/manager.ts +++ b/packages/hwk-trezor-protobuf/src/manager.ts @@ -286,11 +286,14 @@ export const ProtobufManager = () => { // https://github.com/trezor/trezor-firmware/pull/6549 const patchedPayload = messageName === 'Failure' && - binaryPayload.subarray(0, 2).compare(Buffer.from('0811', 'hex')) === 0 + binaryPayload[0] === 0x08 && + binaryPayload[1] === 0x11 ? binaryPayload.subarray(0, 2) : binaryPayload; - const decoded = fromBinary(schema, patchedPayload, { readUnknownFields: false }); + const decoded = fromBinary(schema, Uint8Array.from(patchedPayload), { + readUnknownFields: false, + }); const json = toJson(schema, decoded, { useProtoFieldName: true }); const message = transformSchemaFields('decode', schema, json as Record); diff --git a/yarn.lock b/yarn.lock index e7075eaf0..05c6a22e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4972,6 +4972,32 @@ resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== +"@noble/ciphers@~2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.2.0.tgz#84fb45ac9332925d643b80f89ceb0ea2f21dba95" + integrity sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA== + +"@noble/curves@~2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.2.0.tgz#981be3aadc3bbfbcdb245e78cc97aa6f759246c2" + integrity sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ== + dependencies: + "@noble/hashes" "2.2.0" + +"@noble/hashes@2.2.0", "@noble/hashes@~2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" + integrity sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg== + +"@noble/post-quantum@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@noble/post-quantum/-/post-quantum-0.6.1.tgz#5db8d341cc0335834df185f1fe010782a8c9a1fe" + integrity sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A== + dependencies: + "@noble/ciphers" "~2.2.0" + "@noble/curves" "~2.2.0" + "@noble/hashes" "~2.2.0" + "@noble/curves@1.2.0", "@noble/curves@1.9.7", "@noble/curves@^1.1.0", "@noble/curves@^1.3.0", "@noble/curves@^1.4.2", "@noble/curves@^1.9.7", "@noble/curves@~1.4.0": version "1.9.7" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" From 7054ac49f4ed68350b3585de3ba86d5c05708a9b Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Mon, 27 Jul 2026 23:46:06 +0800 Subject: [PATCH 07/12] feat: add server-owned Ledger attestation relay --- HANDOFF-device-attestation.md | 52 ++- docs/device-attestation-voucher-backend.md | 77 +++- packages/hwk-ledger-adapter/package.json | 16 +- .../src/__tests__/LedgerAdapter.test.ts | 73 ++++ .../src/__tests__/LedgerConnectorBase.test.ts | 76 ++++ .../src/adapter/LedgerAdapter.ts | 84 +++++ .../LedgerAttestationRelayServer.ts | 353 ++++++++++++++++++ .../LedgerAttestationRelayServer.test.ts | 149 ++++++++ .../__tests__/protocol.test.ts | 35 ++ .../__tests__/relayTransport.test.ts | 45 +++ .../src/attestationRelay/index.ts | 25 ++ .../src/attestationRelay/protocol.ts | 184 +++++++++ .../src/attestationRelay/relayTransport.ts | 87 +++++ .../runLedgerDmkGenuineCheck.ts | 129 +++++++ .../src/connector/LedgerConnectorBase.ts | 84 +++++ packages/hwk-ledger-adapter/tsup.config.ts | 6 +- scripts/monitor-config.json | 3 +- 17 files changed, 1460 insertions(+), 18 deletions(-) create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/index.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts create mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index afc68eda5..192f5155a 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -5,9 +5,9 @@ > `docs/device-attestation-voucher-backend.md` 的生产边界为准。 > **重要纠正**:早期 T3W1 真机结果只覆盖 Optiga + Tropic,不能代表当前要求的 -> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。Ledger 的本地 -> DMK Genuine Check 是真实厂商在线验真,但它不是可上传给 OneKey 后端独立信任的 -> 离线证明。 +> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。Ledger 已改成 +> “本地 OneKey 服务端拥有 DMK 状态机、App 只转发物理设备 APDU”;它不是离线证明, +> 但服务端所有权与生产方案一致,后续可把同一套 TypeScript 搬到远端。 --- @@ -16,13 +16,13 @@ | 项 | 状态 | 真机验证 | | ----------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | | Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已补齐流式 proof + Optiga/Tropic/MCU 三层验证 | ⏳ 当前版本待 Safe 7 重新实测;官方 ML-DSA 真向量已通过 | -| Ledger 设备唯一 ID(官方 DMK Genuine Check) | ✅ 本地真实在线验真 | ✅ Nano X 早期版本通过;生产服务端需拥有 DMK 状态机 | +| Ledger 设备唯一 ID(服务端 DMK Genuine Check) | ✅ 本地 TS 服务端 + 一次性 WSS APDU relay 已实现 | ⏳ 新状态机待连接 Nano X 真机跑通 | | Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | | Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | | app-monorepo 调试页(Developer→Gallery→DeviceAuthenticity) | ✅ 已加 | ✅ 已用它测通两端 | | 账户名读取 · Ledger | ✅ Desktop 已实现只读清单 | ✅ 本机读出 Ethereum 名称/地址 | | 账户名读取 · Trezor | ✅ Desktop 已实现 Suite 缓存只读清单 | ✅ 本机读出 Bitcoin 首个 receive address;不连接设备 | -| 本地 mock 发券 | ✅ 本地可信 background 拥有 challenge→ 验真 → 发券全流程 | Trezor 当前版待真机;Ledger 为真实厂商会话但不具备服务端可移植证明 | +| 本地 mock 发券 | ✅ 本地可信服务拥有 challenge→ 验真 → 发券全流程 | Trezor 当前版待真机;Ledger WSS/DMK 集成测试已通过,待真机 | --- @@ -38,8 +38,8 @@ | 仓库 | 分支 | 作用 | | -------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| `~/Development/OnekeyWork/hardware-js-sdk` | `feat/device-attestation-id`(从 `onekey` 切) | SDK 实现(新 `hwk-*` 栈) | -| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `debug/fw-progress-trace-6.5.0` | 调试页 + service 转发方法 | +| `~/Development/OnekeyWork/hardware-js-sdk` | `codex/device-attestation-voucher-proof` | SDK、Ledger 本地/远端 relay server、设备 APDU bridge | +| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `codex/third-party-device-onboarding-rewards` | 设备详情页、Desktop 本地服务启动、Claim 与账户名清单 | | 参考(只读) | — | `~/Development/OnekeyWork/trezor-suite`、`trezor-firmware`、`node_modules/@ledgerhq/device-management-kit`(DMK) | > ⚠️ 关键前提:设备认证只能加在**新 `hwk-*` 栈**(`hwk-trezor-adapter`/`hwk-ledger-adapter`/`hwk-trezor-connector`),**不能**加在 legacy `packages/core`(`DeviceVerify.ts` 那套)——legacy 的 wire protobuf 里没有 `AuthenticateDevice` 消息、features 也没有 `internal_model`。 @@ -108,6 +108,40 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 **真机结果**(Nano X):`Verified ✅`,`deviceId=818706ff78239d1ab642df69c1d1fdd48acfc59ba282f4139383cfee31adfcdc`(走 genuine-check backend)。`attestationPubKey` 字段的真机验证**待做**(清缓存重启 Desktop 后看 Ledger 结果多一行 `attestationPubKey: 04...`)。 +#### 3.2.1 当前“服务端拥有 DMK”实现 + +现在 Claim 不再调用客户端 `verifyDeviceAuthenticity()` 后相信它返回的布尔值。真实路径是: + +1. Desktop background 启动 `LedgerAttestationRelayServer`,只监听 + `127.0.0.1`,创建 32-byte 一次性 token 和 5 分钟 ticket; +2. SDK 的 `LedgerAdapter.runDeviceAttestationApduBridge()` 独占当前已连接 + Ledger session,并暂停 DMK session refresher; +3. App 连接 ticket 的 `ws://127.0.0.1/.../`,只处理 + `apdu-request → exchangeDeviceAttestationApdu → apdu-response`; +4. 服务端自己的 DMK 使用自定义 `TransportFactory`,执行官方 + `GenuineCheckDeviceAction`,并直接连接 Ledger HSM; +5. 只有服务端内部 `ticket.result` 的 `isGenuine + deviceId` 能进入发券函数。 + WSS 发给客户端的 `result` 仅用于 UX,客户端不能提交它来发券; +6. success、failure、断线或取消都会恢复 refresher 并释放设备队列。 + +代码: + +- `packages/hwk-ledger-adapter/src/attestationRelay/` + - `LedgerAttestationRelayServer.ts`:一次性 ticket/WSS 会话; + - `relayTransport.ts`:远端 APDU → DMK `TransportFactory`; + - `runLedgerDmkGenuineCheck.ts`:服务端官方 DMK action; + - `protocol.ts`:严格消息解析、大小/次数/顺序限制。 +- `LedgerConnectorBase.ts`: + `start/exchange/stopDeviceAttestationApduBridge`; +- `LedgerAdapter.ts`:`runDeviceAttestationApduBridge()` 在整个 relay 生命周期独占 + DeviceJobQueue; +- App Desktop: + `ServiceThirdPartyHardware/ledgerLocalAttestationBridge.desktop.ts`。 + +本地与远端的业务代码不变。上线时把 `LedgerAttestationRelayServer.listen()` 放进 +后端进程,用 API 创建 session,把 `ws://127.0.0.1` ticket 换成带登录态/短期 token 的 +`wss://attestation.`;App 的 APDU client 与协议无需改写。 + ### 3.3 Review 修复状态(Fable 5 审查) - **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 代码修复:T3W1 强制 @@ -147,7 +181,9 @@ yarn workspace @onekeyhq/desktop dev **注意**:缓存不能边跑边清(webpack 会立刻重新生成)——必须先停 Desktop 再清。 **构建环境**:`tsup` 曾部分缺失,`yarn install` 可恢复(本会话已跑过一次)。 -> 已构建 + 同步过的包:`hwk-adapter-core`、`hwk-trezor-connector`、`hwk-trezor-adapter`、`hwk-ledger-adapter`(都含最新改动)。 +> 当前使用 `yarn debug:watch` 同步本地 SDK,不发 alpha 包。 +> `scripts/monitor-config.json` 已让 `hwk-ledger-adapter` 同步 `dist` 和 +> `package.json`,因为 `attestation-relay` 是新的 Node-only package subpath。 --- diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md index 0c5909d0a..38129d00d 100644 --- a/docs/device-attestation-voucher-backend.md +++ b/docs/device-attestation-voucher-backend.md @@ -277,17 +277,80 @@ JWS/COSE receipt。单纯透明代理 Ledger WSS、由客户端计算 `isGenuine 证明服务端可独立观察并绑定最终 verdict,否则仍然不够;客户端 `{ verified: true, deviceId }` 永远不能作为发券证据。 -### 4.4 当前本地 mock 的真实性和边界 +#### 4.3.1 可直接部署的 DMK 状态机 + +当前 SDK 已提供 Node-only subpath: + +```ts +import { + LedgerAttestationRelayServer, +} from '@onekeyfe/hwk-ledger-adapter/attestation-relay'; + +const relay = await LedgerAttestationRelayServer.listen({ + host: '127.0.0.1', + port: 0, +}); +const ticket = relay.createSession(); +// ticket.webSocketUrl 给 App;ticket.result 只留在可信服务端。 +const authoritativeResult = await ticket.result; +``` + +`LedgerAttestationRelayServer` 内部运行官方 +`GenuineCheckDeviceAction`,不是模拟 `isGenuine`。它通过自定义 DMK +`TransportFactory` 把每条 APDU 交给 WSS 客户端,客户端再用已连接的 USB/BLE +session 发给物理 Ledger。服务端同时直连 Ledger 官方 HSM endpoint,因此验证时必须 +联网。 + +推荐持久化状态机: + +| 状态 | 允许事件 | 下一个状态 | +| ---------- | ------------------------------------------------- | ---------------------------- | +| `CREATED` | App 使用一次性 token 建立 WSS | `CONNECTED` | +| `CONNECTED`| 首条合法 `hello(version, device metadata)` | `RUNNING` | +| `RUNNING` | 服务端 `apdu-request` / App `apdu-response` | `RUNNING` | +| `RUNNING` | DMK `Completed(isGenuine=true, deviceId present)` | `GENUINE` | +| `RUNNING` | `isGenuine=false` | `NOT_GENUINE` | +| 任意非终态 | 超时、断线、乱序、超限、DMK/HSM/设备错误 | `FAILED` / `EXPIRED` | +| `GENUINE` | claim 事务验证地址签名并成功发券 | `CONSUMED` | +| 任意终态 | 重连、重复 token、重复 claim | 拒绝,状态不回退 | + +WSS 消息: + +```text +App -> Server: hello +Server -> App: ready +Server -> App: apdu-request(requestId, apduHex, timeoutMs) +App -> Server: apdu-response(requestId, dataHex, statusCodeHex) +App -> Server: apdu-error(requestId, message) +Server -> App: interaction / result / error +``` + +实现已经约束: + +- 32-byte CSPRNG token、单次消费、默认 TTL 5 分钟; +- 严格版本/字段/hex/model 校验; +- 同时最多一条未完成 APDU,requestId 必须匹配,拒绝乱序; +- 单 APDU 最大 8 KiB、单会话最多 256 次、单次超时最大 60 秒; +- `isGenuine=false` 时丢弃任何 `deviceId`; +- 只有服务端保存的 Promise/数据库 session 能完成 claim,客户端 `result` 不具备授权力。 + +生产化时保持 `protocol.ts`、`relayTransport.ts` 和 +`runLedgerDmkGenuineCheck.ts` 不变;将内存 ticket store 替换为 Redis/数据库,把本地 +`ws://` 升级为 TLS `wss://`,接入 OneKey 登录态、限流、审计和多实例 sticky routing。 +同一 attestation session 的 WSS 与 DMK runner 必须由同一 worker 拥有,或用严格有序的 +消息队列转发。 + +### 4.4 当前本地服务的真实性和边界 -本地 mock 只替换部署位置,不替换密码学验证: +本地服务只替换部署位置,不替换密码学验证: - Trezor:本地可信 background 生成 32-byte challenge,设备签名,background 使用固定生产根证书重新验证原始 Optiga/Tropic/MCU 证明,验证成功后才生成开发券。 这段 verifier 和 evidence DTO 可以原样移动到后端。 -- Ledger:本地可信 background 直接运行官方 DMK Genuine Check 并连接 Ledger HSM, - 同一个函数拿到真实 verdict 后才生成开发券。它验证了设备、SDK 和厂商网络流程, - 但结果不能作为可上传的服务端证明;生产仍需上一节的“服务端 DMK + 远程 APDU - transport”或厂商签名 receipt。 +- Ledger:本地可信 Node 服务运行官方 DMK Genuine Check 并连接 Ledger HSM;App + background 只转发 APDU。服务端内部 ticket 拿到真实 verdict + DSID 后才生成开发券。 + 这已经是上一节的“服务端 DMK + 远程 APDU transport”,只是 WSS 当前监听 + `127.0.0.1`;迁到远端后不改变信任边界。 - `runTrustedLocalMockDeviceClaim` 接收的是执行真实验真的闭包,不接受 renderer 提交的 attestation DTO;challenge 在执行器内部生成,领券前不会交给不可信调用方 决定验证结果。 @@ -298,7 +361,7 @@ JWS/COSE receipt。单纯透明代理 Ledger WSS、由客户端计算 `isGenuine | ----------------------------------- | ------------------------------- | ----------------- | ------------------------------- | --------------------------------- | | Trezor 本地证书验证 | 支持,USB/BLE 取 proof | WebUSB 可用时支持 | 有对应 USB/BLE transport 时支持 | 不需要;拿到 proof 后可完全离线验 | | Trezor 后端发券 | 支持 | 支持 | 支持 | 需要连接 OneKey 后端提交 proof | -| Ledger 当前本地 DMK mock | 支持 WebHID/BLE | WebHID 可用时支持 | DMK BLE transport 可用时支持 | 必须连接 Ledger HSM | +| Ledger 当前本地 DMK server | 支持 WebHID/BLE | Node relay 不内置 | 当前本地 server 仅 Desktop | 必须连接 Ledger HSM | | Ledger 生产服务端 DMK + APDU bridge | 支持 | 支持 | 支持 | 必须连接 OneKey 后端和 Ledger HSM | 操作系统不是主要限制,真正的限制是该端是否具备对应 USB/BLE transport。Trezor diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index a095af3e5..fd7c11d24 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -17,6 +17,16 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" } + }, + "./attestation-relay": { + "import": { + "types": "./dist/attestation-relay.d.mts", + "default": "./dist/attestation-relay.mjs" + }, + "require": { + "types": "./dist/attestation-relay.d.ts", + "default": "./dist/attestation-relay.js" + } } }, "files": [ @@ -52,9 +62,13 @@ "@ledgerhq/hw-transport": "^6.34.1", "@noble/hashes": "^1.3.3", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" + "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1", + "purify-ts": "2.1.0", + "rxjs": "7.8.2", + "ws": "^7.5.10" }, "devDependencies": { + "@types/ws": "^8.5.5", "rimraf": "^5.0.0", "tsup": "^8.0.0", "typescript": "^5.4.0" diff --git a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts index 00417519d..eb5717659 100644 --- a/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts +++ b/packages/hwk-ledger-adapter/src/__tests__/LedgerAdapter.test.ts @@ -252,6 +252,79 @@ describe('LedgerAdapter', () => { ]); }); + it('reserves the device while a server-owned APDU bridge is running', async () => { + connector.callImpl.mockImplementation( + async (_sessionId: string, method: string, params: unknown) => { + if (method === 'startDeviceAttestationApduBridge') { + return { + id: 'physical-ledger', + modelId: 'nanoX', + name: 'Ledger Nano X', + connectionType: 'USB', + }; + } + if (method === 'exchangeDeviceAttestationApdu') { + expect(params).toEqual({ apduHex: 'e001000000', timeoutMs: 2_000 }); + return { dataHex: 'abcd', statusCodeHex: '9000' }; + } + return {}; + } + ); + + const result = await adapter.runDeviceAttestationApduBridge( + 'dev-1', + async bridge => { + expect(bridge.device).toMatchObject({ + id: 'physical-ledger', + modelId: 'nanoX', + }); + return bridge.exchangeApdu('e001000000', 2_000); + } + ); + + expect(result).toEqual({ + success: true, + payload: { dataHex: 'abcd', statusCodeHex: '9000' }, + }); + expect(connector.callImpl.mock.calls.map(call => call[1])).toEqual([ + 'startDeviceAttestationApduBridge', + 'exchangeDeviceAttestationApdu', + 'stopDeviceAttestationApduBridge', + ]); + }); + + it('always releases the server-owned APDU bridge after a relay failure', async () => { + connector.callImpl.mockImplementation( + async (_sessionId: string, method: string) => { + if (method === 'startDeviceAttestationApduBridge') { + return { + id: 'physical-ledger', + modelId: 'nanoX', + connectionType: 'USB', + }; + } + return {}; + } + ); + + const result = await adapter.runDeviceAttestationApduBridge( + 'dev-1', + async () => { + throw new Error('local WSS disconnected'); + } + ); + + expect(result).toMatchObject({ + success: false, + payload: { error: expect.stringContaining('local WSS disconnected') }, + }); + expect(connector.callImpl).toHaveBeenLastCalledWith( + 'session-abc', + 'stopDeviceAttestationApduBridge', + {} + ); + }); + describe('searchDevices', () => { it('should return devices from connector', async () => { const devices = await adapter.searchDevices(); diff --git a/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts b/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts index 440296aa1..59b217f67 100644 --- a/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts +++ b/packages/hwk-ledger-adapter/src/__tests__/LedgerConnectorBase.test.ts @@ -122,6 +122,82 @@ describe('LedgerConnectorBase runtime genuine-check relay', () => { }); }); +describe('LedgerConnectorBase server-owned APDU bridge', () => { + it('pauses the session refresher and forwards raw APDUs on the existing session', async () => { + const releaseRefresher = jest.fn(); + const dmk = { + disableDeviceSessionRefresher: jest.fn().mockReturnValue(releaseRefresher), + getConnectedDevice: jest.fn().mockReturnValue({ + id: 'physical-ledger', + modelId: 'nanoX', + name: 'Ledger Nano X', + type: 'USB', + }), + sendApdu: jest.fn().mockResolvedValue({ + data: Uint8Array.from([0xab, 0xcd]), + statusCode: Uint8Array.from([0x90, 0x00]), + }), + }; + const connector = new LedgerConnectorBase(async () => ({}), { + dmk: dmk as any, + }); + + await expect( + connector.call('session-1', 'startDeviceAttestationApduBridge', {}) + ).resolves.toEqual({ + success: true, + payload: { + id: 'physical-ledger', + modelId: 'nanoX', + name: 'Ledger Nano X', + connectionType: 'USB', + }, + }); + await expect( + connector.call('session-1', 'exchangeDeviceAttestationApdu', { + apduHex: 'e001000000', + timeoutMs: 2_000, + }) + ).resolves.toEqual({ + success: true, + payload: { + dataHex: 'abcd', + statusCodeHex: '9000', + }, + }); + await connector.call('session-1', 'stopDeviceAttestationApduBridge', {}); + + expect(dmk.disableDeviceSessionRefresher).toHaveBeenCalledWith({ + sessionId: 'session-1', + blockerId: 'onekey-ledger-attestation-relay', + }); + expect(dmk.sendApdu).toHaveBeenCalledWith({ + sessionId: 'session-1', + apdu: Uint8Array.from([0xe0, 0x01, 0x00, 0x00, 0x00]), + abortTimeout: 2_000, + triggersDisconnection: false, + }); + expect(releaseRefresher).toHaveBeenCalledTimes(1); + }); + + it('rejects APDUs when no server bridge owns the session', async () => { + const connector = new LedgerConnectorBase(async () => ({}), { + dmk: {} as any, + }); + + const result = await connector.call( + 'session-1', + 'exchangeDeviceAttestationApdu', + { apduHex: 'e001000000' } + ); + + expect(result).toMatchObject({ + success: false, + error: { message: expect.stringContaining('not active') }, + }); + }); +}); + describe('LedgerConnectorBase BLE discovery', () => { it('allows transport ids as BLE connectId even when they are not four-character names', async () => { const connector = new SearchConnector( diff --git a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts index faa8676b1..90dc09508 100644 --- a/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts +++ b/packages/hwk-ledger-adapter/src/adapter/LedgerAdapter.ts @@ -153,6 +153,21 @@ export type LedgerAttestationResult = { note: string; }; +export type LedgerAttestationBridgeDevice = { + id: string; + modelId: 'nanoS' | 'nanoSP' | 'nanoX' | 'stax' | 'flex' | 'apexp'; + name?: string; + connectionType?: 'USB' | 'BLE'; +}; + +export type LedgerAttestationApduBridge = { + device: LedgerAttestationBridgeDevice; + exchangeApdu: ( + apduHex: string, + timeoutMs?: number + ) => Promise<{ dataHex: string; statusCodeHex: string }>; +}; + export class LedgerAdapter implements IHardwareWallet { readonly vendor = 'ledger' as const; @@ -856,6 +871,75 @@ export class LedgerAdapter implements IHardwareWallet { } } + /** + * Reserves the existing physical Ledger session while a server-owned DMK + * Genuine Check drives it. The callback can only exchange raw APDUs; the + * authoritative verdict remains in the server state machine. + */ + async runDeviceAttestationApduBridge( + connectId: string, + run: (bridge: LedgerAttestationApduBridge) => Promise + ): Promise> { + const queueKey = connectId || '__ledger_default__'; + try { + const payload = await this._jobQueue.enqueue( + queueKey, + async signal => { + const device = (await this._runConnectorCall( + connectId, + 'startDeviceAttestationApduBridge', + {}, + signal + )) as LedgerAttestationBridgeDevice; + const sessionId = + this._sessions.get(connectId) ?? + (this._sessions.size === 1 + ? this._sessions.values().next().value + : undefined); + if (!sessionId) { + throw new Error( + 'Ledger attestation bridge started without an active device session' + ); + } + try { + return await run({ + device, + exchangeApdu: async (apduHex, timeoutMs) => + (await this._callConnector( + sessionId, + 'exchangeDeviceAttestationApdu', + { apduHex, timeoutMs }, + signal + )) as { dataHex: string; statusCodeHex: string }, + }); + } finally { + try { + await this._callConnector( + sessionId, + 'stopDeviceAttestationApduBridge', + {} + ); + } catch { + // reset() also releases the refresher blocker if the session died. + this.connector.reset(); + this.resetState(); + } + } + }, + { + label: 'ledgerDeviceAttestationApduBridge', + rejectIfBusy: true, + busyError: LedgerAdapter._createDeviceBusyError( + 'ledgerDeviceAttestationApduBridge' + ), + } + ); + return success(payload); + } catch (error) { + return this.errorToFailure(error); + } + } + /** * Runs Ledger's official genuine check (DMK GenuineCheckDeviceAction) over the * SAME secure-channel backend as app install diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts b/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts new file mode 100644 index 000000000..37cf46f18 --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts @@ -0,0 +1,353 @@ +import { randomBytes, randomUUID } from 'node:crypto'; +import { createServer } from 'node:http'; + +import WebSocket from 'ws'; + +import { + DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, + LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, + MAX_LEDGER_RELAY_APDU_BYTES, + MAX_LEDGER_RELAY_APDU_EXCHANGES, + parseLedgerRelayClientMessage, +} from './protocol'; +import { runLedgerDmkGenuineCheck } from './runLedgerDmkGenuineCheck'; + +import type { AddressInfo } from 'node:net'; +import type { + LedgerRelayClientMessage, + LedgerRelayDevice, + LedgerRelayServerMessage, +} from './protocol'; +import type { + LedgerRelayApduBridge, + LedgerRelayApduResponse, +} from './relayTransport'; +import type { LedgerServerGenuineCheckResult } from './runLedgerDmkGenuineCheck'; + +type RelaySession = { + expiresAt: number; + consumed: boolean; + expiryTimer: ReturnType; + resolve: (result: LedgerServerGenuineCheckResult) => void; + reject: (error: Error) => void; +}; + +type PendingApdu = { + requestId: string; + resolve: (response: LedgerRelayApduResponse) => void; + reject: (error: Error) => void; + timer: ReturnType; +}; + +export type LedgerAttestationRelayTicket = { + webSocketUrl: string; + expiresAt: number; + result: Promise; +}; + +export type RunLedgerServerGenuineCheck = ( + bridge: LedgerRelayApduBridge, + device: LedgerRelayDevice +) => Promise; + +const sendMessage = (socket: WebSocket, message: LedgerRelayServerMessage) => { + socket.send(JSON.stringify(message)); +}; + +const asError = (error: unknown): Error => + error instanceof Error ? error : new Error(String(error)); + +export class LedgerAttestationRelayServer { + private readonly sessions = new Map(); + + private constructor( + private readonly httpServer: ReturnType, + private readonly webSocketServer: WebSocket.Server, + private readonly baseUrl: string, + private readonly sessionTtlMs: number, + private readonly runGenuineCheck: RunLedgerServerGenuineCheck + ) { + webSocketServer.on('connection', (socket, request) => { + this.handleConnection(socket, request.url ?? '/'); + }); + } + + static async listen(options?: { + host?: string; + port?: number; + sessionTtlMs?: number; + publicWebSocketBaseUrl?: string; + runGenuineCheck?: RunLedgerServerGenuineCheck; + }): Promise { + const host = options?.host ?? '127.0.0.1'; + if (options?.publicWebSocketBaseUrl) { + let parsedPublicUrl: URL; + try { + parsedPublicUrl = new URL(options.publicWebSocketBaseUrl); + } catch { + throw new Error('Ledger relay public WebSocket base URL is invalid'); + } + if ( + (parsedPublicUrl.protocol !== 'ws:' && + parsedPublicUrl.protocol !== 'wss:') || + parsedPublicUrl.username || + parsedPublicUrl.password || + parsedPublicUrl.search || + parsedPublicUrl.hash + ) { + throw new Error('Ledger relay public WebSocket base URL is invalid'); + } + } + const httpServer = createServer(); + const webSocketServer = new WebSocket.Server({ + server: httpServer, + maxPayload: MAX_LEDGER_RELAY_APDU_BYTES * 2 + 4_096, + }); + await new Promise((resolve, reject) => { + httpServer.once('error', reject); + httpServer.listen(options?.port ?? 0, host, () => { + httpServer.off('error', reject); + resolve(); + }); + }); + const address = httpServer.address() as AddressInfo; + const publicWebSocketBaseUrl = + options?.publicWebSocketBaseUrl ?? `ws://${host}:${address.port}`; + return new LedgerAttestationRelayServer( + httpServer, + webSocketServer, + publicWebSocketBaseUrl.replace(/\/+$/, ''), + options?.sessionTtlMs ?? DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, + options?.runGenuineCheck ?? runLedgerDmkGenuineCheck + ); + } + + createSession(): LedgerAttestationRelayTicket { + const token = randomBytes(32).toString('hex'); + const expiresAt = Date.now() + this.sessionTtlMs; + let resolve!: (result: LedgerServerGenuineCheckResult) => void; + let reject!: (error: Error) => void; + const result = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const expiryTimer = setTimeout(() => { + const session = this.sessions.get(token); + if (!session || session.consumed) return; + this.sessions.delete(token); + session.reject(new Error('Ledger attestation relay session expired')); + }, this.sessionTtlMs); + expiryTimer.unref?.(); + this.sessions.set(token, { + expiresAt, + consumed: false, + expiryTimer, + resolve, + reject, + }); + return { + webSocketUrl: `${this.baseUrl}/v1/ledger/attestation/${token}`, + expiresAt, + result, + }; + } + + private handleConnection(socket: WebSocket, rawUrl: string): void { + let parsed: URL; + try { + parsed = new URL(rawUrl, this.baseUrl); + } catch { + socket.close(4400, 'invalid-url'); + return; + } + if (parsed.search || parsed.hash) { + socket.close(4400, 'invalid-url'); + return; + } + const match = parsed.pathname.match( + /^\/v1\/ledger\/attestation\/([0-9a-f]{64})$/ + ); + const token = match?.[1]; + if (!token) { + socket.close(4404, 'session-not-found'); + return; + } + const session = this.sessions.get(token); + if ( + !session || + session.consumed || + session.expiresAt <= Date.now() + ) { + socket.close(4404, 'session-not-found'); + return; + } + session.consumed = true; + clearTimeout(session.expiryTimer); + this.sessions.delete(token); + let helloReceived = false; + let finished = false; + let exchangeCount = 0; + let pendingApdu: PendingApdu | undefined; + + const fail = (error: unknown, closeCode = 4400) => { + if (finished) return; + finished = true; + const normalized = asError(error); + if (pendingApdu) { + clearTimeout(pendingApdu.timer); + pendingApdu.reject(normalized); + pendingApdu = undefined; + } + session.reject(normalized); + if (socket.readyState === WebSocket.OPEN) { + sendMessage(socket, { + type: 'error', + code: 'ledger_attestation_failed', + message: normalized.message, + }); + socket.close(closeCode, 'attestation-failed'); + } + }; + + const bridge: LedgerRelayApduBridge = { + exchangeApdu: (apdu, timeoutMs = 30_000) => { + if (finished || socket.readyState !== WebSocket.OPEN) { + return Promise.reject(new Error('Ledger relay client disconnected')); + } + if (pendingApdu) { + return Promise.reject( + new Error('Ledger relay permits only one outstanding APDU') + ); + } + if (apdu.byteLength > MAX_LEDGER_RELAY_APDU_BYTES) { + return Promise.reject(new Error('Ledger relay APDU is too large')); + } + exchangeCount += 1; + if (exchangeCount > MAX_LEDGER_RELAY_APDU_EXCHANGES) { + return Promise.reject( + new Error('Ledger relay APDU exchange limit exceeded') + ); + } + const requestId = randomUUID(); + return new Promise((resolve, reject) => { + const boundedTimeout = Math.max(1_000, Math.min(timeoutMs, 60_000)); + const timer = setTimeout(() => { + pendingApdu = undefined; + reject(new Error('Ledger relay APDU timed out')); + }, boundedTimeout); + pendingApdu = { requestId, resolve, reject, timer }; + sendMessage(socket, { + type: 'apdu-request', + requestId, + apduHex: Buffer.from(apdu).toString('hex'), + timeoutMs: boundedTimeout, + }); + }); + }, + onInteraction: requiredUserInteraction => { + if (!finished && socket.readyState === WebSocket.OPEN) { + sendMessage(socket, { + type: 'interaction', + requiredUserInteraction, + }); + } + }, + }; + + const handleClientMessage = (message: LedgerRelayClientMessage) => { + if (!helloReceived) { + if (message.type !== 'hello') { + throw new Error('Ledger relay expected hello first'); + } + helloReceived = true; + sendMessage(socket, { + type: 'ready', + protocolVersion: LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, + }); + void this.runGenuineCheck(bridge, message.device).then( + result => { + if (finished) return; + if ( + result.isGenuine === true && + (!result.deviceId || !/^[0-9a-f]{64}$/i.test(result.deviceId)) + ) { + fail( + new Error( + 'Ledger server Genuine Check succeeded without a valid physical-device DSID' + ) + ); + return; + } + finished = true; + const authoritativeResult: LedgerServerGenuineCheckResult = + result.isGenuine === true + ? result + : { isGenuine: false, deviceId: undefined }; + session.resolve(authoritativeResult); + sendMessage(socket, { + type: 'result', + isGenuine: authoritativeResult.isGenuine, + deviceId: authoritativeResult.deviceId, + }); + socket.close(1000, 'complete'); + }, + error => fail(error) + ); + return; + } + if (message.type !== 'apdu-response' && message.type !== 'apdu-error') { + throw new Error('Unexpected Ledger relay message'); + } + if (!pendingApdu || pendingApdu.requestId !== message.requestId) { + throw new Error('Ledger relay APDU response is out of order'); + } + const pending = pendingApdu; + pendingApdu = undefined; + clearTimeout(pending.timer); + if (message.type === 'apdu-error') { + pending.reject(new Error(message.message)); + } else { + pending.resolve({ + data: Uint8Array.from(Buffer.from(message.dataHex, 'hex')), + statusCode: Uint8Array.from( + Buffer.from(message.statusCodeHex, 'hex') + ), + }); + } + }; + + socket.on('message', raw => { + try { + handleClientMessage(parseLedgerRelayClientMessage(raw.toString())); + } catch (error) { + fail(error); + } + }); + socket.on('close', () => { + if (!finished) { + fail(new Error('Ledger relay client disconnected'), 4408); + } + }); + socket.on('error', error => fail(error, 1011)); + } + + async close(): Promise { + const error = new Error('Ledger attestation relay server closed'); + for (const session of this.sessions.values()) { + clearTimeout(session.expiryTimer); + if (!session.consumed) { + session.reject(error); + } + } + this.sessions.clear(); + for (const socket of this.webSocketServer.clients) { + socket.close(1001, 'server-closed'); + } + await new Promise(resolve => { + this.webSocketServer.close(() => resolve()); + }); + await new Promise(resolve => { + this.httpServer.close(() => resolve()); + }); + } +} diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts new file mode 100644 index 000000000..de0ddde2b --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts @@ -0,0 +1,149 @@ +import WebSocket from 'ws'; + +import { LedgerAttestationRelayServer } from '../LedgerAttestationRelayServer'; + +import type { LedgerRelayClientMessage, LedgerRelayServerMessage } from '../protocol'; + +const waitForOpen = (socket: WebSocket) => + new Promise((resolve, reject) => { + socket.once('open', resolve); + socket.once('error', reject); + }); + +describe('LedgerAttestationRelayServer', () => { + let server: LedgerAttestationRelayServer | undefined; + + afterEach(async () => { + await server?.close(); + server = undefined; + }); + + it('owns the DMK result while the client only forwards APDUs', async () => { + const runGenuineCheck = jest.fn(async bridge => { + const response = await bridge.exchangeApdu( + Uint8Array.from([0xe0, 0x01, 0x00, 0x00, 0x00]), + 2_000 + ); + expect(Buffer.from(response.data).toString('hex')).toBe('abcd'); + expect(Buffer.from(response.statusCode).toString('hex')).toBe('9000'); + return { + isGenuine: true, + deviceId: 'ab'.repeat(32), + }; + }); + server = await LedgerAttestationRelayServer.listen({ + host: '127.0.0.1', + port: 0, + runGenuineCheck, + }); + const ticket = server.createSession(); + const socket = new WebSocket(ticket.webSocketUrl); + await waitForOpen(socket); + + socket.on('message', raw => { + const message = JSON.parse(raw.toString()) as LedgerRelayServerMessage; + if (message.type === 'apdu-request') { + socket.send( + JSON.stringify({ + type: 'apdu-response', + requestId: message.requestId, + dataHex: 'abcd', + statusCodeHex: '9000', + } satisfies LedgerRelayClientMessage) + ); + } + }); + socket.send( + JSON.stringify({ + type: 'hello', + protocolVersion: 1, + device: { + id: 'local-ledger', + modelId: 'nanoX', + name: 'Ledger Nano X', + }, + } satisfies LedgerRelayClientMessage) + ); + + await expect(ticket.result).resolves.toEqual({ + isGenuine: true, + deviceId: 'ab'.repeat(32), + }); + expect(runGenuineCheck).toHaveBeenCalledTimes(1); + expect(runGenuineCheck.mock.calls[0][1]).toMatchObject({ + id: 'local-ledger', + modelId: 'nanoX', + name: 'Ledger Nano X', + }); + }); + + it('rejects reuse of a consumed single-use session token', async () => { + server = await LedgerAttestationRelayServer.listen({ + host: '127.0.0.1', + port: 0, + runGenuineCheck: async () => ({ + isGenuine: true, + deviceId: 'cd'.repeat(32), + }), + }); + const ticket = server.createSession(); + const first = new WebSocket(ticket.webSocketUrl); + await waitForOpen(first); + first.send( + JSON.stringify({ + type: 'hello', + protocolVersion: 1, + device: { id: 'ledger', modelId: 'nanoX' }, + } satisfies LedgerRelayClientMessage) + ); + await ticket.result; + + const second = new WebSocket(ticket.webSocketUrl); + const closeCode = await new Promise((resolve, reject) => { + second.once('close', resolve); + second.once('error', reject); + }); + expect(closeCode).toBe(4404); + }); + + it('supports a TLS reverse-proxy public WSS base for production deployment', async () => { + server = await LedgerAttestationRelayServer.listen({ + host: '127.0.0.1', + port: 0, + publicWebSocketBaseUrl: 'wss://attestation.onekey.test/', + runGenuineCheck: async () => ({ + isGenuine: true, + deviceId: 'ef'.repeat(32), + }), + }); + + const ticket = server.createSession(); + void ticket.result.catch(() => undefined); + expect(ticket.webSocketUrl).toMatch( + /^wss:\/\/attestation\.onekey\.test\/v1\/ledger\/attestation\/[0-9a-f]{64}$/ + ); + }); + + it('fails closed if a genuine verdict has no valid physical-device DSID', async () => { + server = await LedgerAttestationRelayServer.listen({ + host: '127.0.0.1', + port: 0, + runGenuineCheck: async () => ({ + isGenuine: true, + deviceId: 'client-controlled-value', + }), + }); + const ticket = server.createSession(); + const socket = new WebSocket(ticket.webSocketUrl); + await waitForOpen(socket); + socket.send( + JSON.stringify({ + type: 'hello', + protocolVersion: 1, + device: { id: 'ledger', modelId: 'nanoX' }, + } satisfies LedgerRelayClientMessage) + ); + + await expect(ticket.result).rejects.toThrow('valid physical-device DSID'); + }); +}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts new file mode 100644 index 000000000..592749d66 --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts @@ -0,0 +1,35 @@ +import { + MAX_LEDGER_RELAY_APDU_BYTES, + parseLedgerRelayClientMessage, +} from '../protocol'; + +describe('Ledger attestation relay protocol', () => { + it('rejects oversized APDU responses before allocating relay state', () => { + expect(() => + parseLedgerRelayClientMessage( + JSON.stringify({ + type: 'apdu-response', + requestId: 'request-1', + dataHex: 'aa'.repeat(MAX_LEDGER_RELAY_APDU_BYTES + 1), + statusCodeHex: '9000', + }) + ) + ).toThrow('too large'); + }); + + it('rejects malformed status words and unknown messages', () => { + expect(() => + parseLedgerRelayClientMessage( + JSON.stringify({ + type: 'apdu-response', + requestId: 'request-1', + dataHex: '', + statusCodeHex: '90', + }) + ) + ).toThrow('statusCodeHex'); + expect(() => parseLedgerRelayClientMessage('{"type":"verified","verified":true}')).toThrow( + 'Unsupported' + ); + }); +}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts new file mode 100644 index 000000000..3af6b23a2 --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts @@ -0,0 +1,45 @@ +import { firstValueFrom } from 'rxjs'; + +import { createLedgerRelayTransportFactory } from '../relayTransport'; + +describe('createLedgerRelayTransportFactory', () => { + it('adapts a remote APDU bridge to the Ledger DMK TransportFactory contract', async () => { + const bridge = { + exchangeApdu: jest.fn(async () => ({ + data: Uint8Array.from([0x12, 0x34]), + statusCode: Uint8Array.from([0x90, 0x00]), + })), + }; + const transport = createLedgerRelayTransportFactory({ + bridge, + device: { id: 'relay-device', modelId: 'nanoX', name: 'Ledger Nano X' }, + })({ + deviceModelDataSource: { + getDeviceModel: jest.fn().mockReturnValue({ id: 'nanoX' }), + }, + } as any); + + await expect(firstValueFrom(transport.startDiscovering())).resolves.toMatchObject({ + id: 'relay-device', + name: 'Ledger Nano X', + transport: 'ONEKEY_LEDGER_ATTESTATION_RELAY', + }); + const connected = await transport.connect({ + deviceId: 'relay-device', + onDisconnect: jest.fn(), + }); + expect(connected.isRight()).toBe(true); + const device = connected.extract() as { + sendApdu: (apdu: Uint8Array) => Promise<{ + isRight: () => boolean; + extract: () => { data: Uint8Array; statusCode: Uint8Array }; + }>; + }; + const response = await device.sendApdu(Uint8Array.from([0xe0, 0x01])); + expect(response.isRight()).toBe(true); + expect(response.extract()).toMatchObject({ + data: Uint8Array.from([0x12, 0x34]), + statusCode: Uint8Array.from([0x90, 0x00]), + }); + }); +}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/index.ts b/packages/hwk-ledger-adapter/src/attestationRelay/index.ts new file mode 100644 index 000000000..f7d0c8b6d --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/index.ts @@ -0,0 +1,25 @@ +export { + LedgerAttestationRelayServer, + type LedgerAttestationRelayTicket, + type RunLedgerServerGenuineCheck, +} from './LedgerAttestationRelayServer'; +export { + createLedgerRelayTransportFactory, + LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + type LedgerRelayApduBridge, + type LedgerRelayApduResponse, +} from './relayTransport'; +export { + DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, + LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, + MAX_LEDGER_RELAY_APDU_BYTES, + MAX_LEDGER_RELAY_APDU_EXCHANGES, + parseLedgerRelayClientMessage, + type LedgerRelayClientMessage, + type LedgerRelayDevice, + type LedgerRelayServerMessage, +} from './protocol'; +export { + runLedgerDmkGenuineCheck, + type LedgerServerGenuineCheckResult, +} from './runLedgerDmkGenuineCheck'; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts b/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts new file mode 100644 index 000000000..d3b2e07c8 --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts @@ -0,0 +1,184 @@ +export const LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION = 1 as const; +export const MAX_LEDGER_RELAY_APDU_BYTES = 8 * 1024; +export const MAX_LEDGER_RELAY_APDU_EXCHANGES = 256; +export const DEFAULT_LEDGER_RELAY_SESSION_TTL_MS = 5 * 60_000; + +const REQUEST_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/; +const HEX_PATTERN = /^[0-9a-f]*$/i; +const LEDGER_MODEL_IDS = new Set(['nanoS', 'nanoSP', 'nanoX', 'stax', 'flex', 'apexp']); + +export type LedgerRelayDevice = { + id: string; + modelId: 'nanoS' | 'nanoSP' | 'nanoX' | 'stax' | 'flex' | 'apexp'; + name?: string; + connectionType?: 'USB' | 'BLE'; +}; + +export type LedgerRelayHelloMessage = { + type: 'hello'; + protocolVersion: typeof LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION; + device: LedgerRelayDevice; +}; + +export type LedgerRelayApduResponseMessage = { + type: 'apdu-response'; + requestId: string; + dataHex: string; + statusCodeHex: string; +}; + +export type LedgerRelayApduErrorMessage = { + type: 'apdu-error'; + requestId: string; + message: string; +}; + +export type LedgerRelayClientMessage = + | LedgerRelayHelloMessage + | LedgerRelayApduResponseMessage + | LedgerRelayApduErrorMessage; + +export type LedgerRelayServerMessage = + | { + type: 'ready'; + protocolVersion: typeof LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION; + } + | { + type: 'apdu-request'; + requestId: string; + apduHex: string; + timeoutMs: number; + } + | { + type: 'interaction'; + requiredUserInteraction: string; + } + | { + type: 'result'; + isGenuine: boolean; + deviceId?: string; + } + | { + type: 'error'; + code: string; + message: string; + }; + +const assertHex = ({ + value, + field, + exactBytes, + maxBytes, +}: { + value: unknown; + field: string; + exactBytes?: number; + maxBytes?: number; +}): string => { + if ( + typeof value !== 'string' || + value.length % 2 !== 0 || + !HEX_PATTERN.test(value) || + (exactBytes !== undefined && value.length !== exactBytes * 2) + ) { + throw new Error(`Invalid Ledger relay ${field}`); + } + if (maxBytes !== undefined && value.length / 2 > maxBytes) { + throw new Error(`Ledger relay ${field} is too large`); + } + return value.toLowerCase(); +}; + +const assertRequestId = (value: unknown): string => { + if (typeof value !== 'string' || !REQUEST_ID_PATTERN.test(value)) { + throw new Error('Invalid Ledger relay requestId'); + } + return value; +}; + +const parseDevice = (value: unknown): LedgerRelayDevice => { + if (!value || typeof value !== 'object') { + throw new Error('Invalid Ledger relay device'); + } + const device = value as Record; + if ( + typeof device.id !== 'string' || + device.id.length < 1 || + device.id.length > 128 || + typeof device.modelId !== 'string' || + !LEDGER_MODEL_IDS.has(device.modelId) + ) { + throw new Error('Invalid Ledger relay device metadata'); + } + if ( + device.name !== undefined && + (typeof device.name !== 'string' || device.name.length > 128) + ) { + throw new Error('Invalid Ledger relay device name'); + } + if ( + device.connectionType !== undefined && + device.connectionType !== 'USB' && + device.connectionType !== 'BLE' + ) { + throw new Error('Invalid Ledger relay connection type'); + } + return device as LedgerRelayDevice; +}; + +export const parseLedgerRelayClientMessage = (raw: string): LedgerRelayClientMessage => { + if (Buffer.byteLength(raw, 'utf8') > MAX_LEDGER_RELAY_APDU_BYTES * 2 + 4_096) { + throw new Error('Ledger relay message is too large'); + } + let value: unknown; + try { + value = JSON.parse(raw); + } catch { + throw new Error('Invalid Ledger relay JSON'); + } + if (!value || typeof value !== 'object') { + throw new Error('Invalid Ledger relay message'); + } + const message = value as Record; + if (message.type === 'hello') { + if (message.protocolVersion !== LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION) { + throw new Error('Unsupported Ledger relay protocol version'); + } + return { + type: 'hello', + protocolVersion: LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, + device: parseDevice(message.device), + }; + } + if (message.type === 'apdu-response') { + return { + type: 'apdu-response', + requestId: assertRequestId(message.requestId), + dataHex: assertHex({ + value: message.dataHex, + field: 'dataHex', + maxBytes: MAX_LEDGER_RELAY_APDU_BYTES, + }), + statusCodeHex: assertHex({ + value: message.statusCodeHex, + field: 'statusCodeHex', + exactBytes: 2, + }), + }; + } + if (message.type === 'apdu-error') { + if ( + typeof message.message !== 'string' || + message.message.length < 1 || + message.message.length > 512 + ) { + throw new Error('Invalid Ledger relay APDU error'); + } + return { + type: 'apdu-error', + requestId: assertRequestId(message.requestId), + message: message.message, + }; + } + throw new Error('Unsupported Ledger relay message'); +}; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts b/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts new file mode 100644 index 000000000..4d99e10ab --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts @@ -0,0 +1,87 @@ +import { + ApduResponse, + GeneralDmkError, + TransportConnectedDevice, + UnknownDeviceError, +} from '@ledgerhq/device-management-kit'; +import { Left, Right } from 'purify-ts'; +import { of } from 'rxjs'; + +import type { + DeviceModelId, + Transport, + TransportFactory, +} from '@ledgerhq/device-management-kit'; + +import type { LedgerRelayDevice } from './protocol'; + +export const LEDGER_ATTESTATION_RELAY_TRANSPORT_ID = + 'ONEKEY_LEDGER_ATTESTATION_RELAY'; + +export type LedgerRelayApduResponse = { + data: Uint8Array; + statusCode: Uint8Array; +}; + +export type LedgerRelayApduBridge = { + exchangeApdu: ( + apdu: Uint8Array, + timeoutMs?: number + ) => Promise; + onInteraction?: (requiredUserInteraction: string) => void; +}; + +export const createLedgerRelayTransportFactory = ({ + bridge, + device, +}: { + bridge: LedgerRelayApduBridge; + device: LedgerRelayDevice; +}): TransportFactory => args => { + const deviceModel = args.deviceModelDataSource.getDeviceModel({ + id: device.modelId as DeviceModelId, + }); + const discoveredDevice = { + id: device.id, + deviceModel, + transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + name: device.name, + }; + let connectedDevice: TransportConnectedDevice | undefined; + + const transport: Transport = { + getIdentifier: () => LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + isSupported: () => true, + startDiscovering: () => of(discoveredDevice), + stopDiscovering: () => undefined, + listenToAvailableDevices: () => of([discoveredDevice]), + connect: async ({ deviceId }) => { + if (deviceId !== device.id) { + return Left(new UnknownDeviceError()); + } + if (!connectedDevice) { + connectedDevice = new TransportConnectedDevice({ + id: device.id, + deviceModel, + type: device.connectionType ?? 'USB', + transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + name: device.name, + sendApdu: async (apdu, _triggersDisconnection, abortTimeout) => { + try { + const response = await bridge.exchangeApdu(apdu, abortTimeout); + return Right(new ApduResponse(response)); + } catch (error) { + return Left(new GeneralDmkError(error)); + } + }, + }); + } + return Right(connectedDevice); + }, + disconnect: async () => { + connectedDevice = undefined; + return Right(undefined); + }, + }; + return transport; +}; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts b/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts new file mode 100644 index 000000000..6ce1126a1 --- /dev/null +++ b/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts @@ -0,0 +1,129 @@ +import { + DeviceActionStatus, + DeviceManagementKitBuilder, + GenuineCheckDeviceAction, +} from '@ledgerhq/device-management-kit'; +import { firstValueFrom, take, timeout } from 'rxjs'; + +import { + LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + createLedgerRelayTransportFactory, +} from './relayTransport'; + +import type { LedgerRelayApduBridge } from './relayTransport'; +import type { LedgerRelayDevice } from './protocol'; + +export type LedgerServerGenuineCheckResult = { + isGenuine: boolean; + deviceId?: string; +}; + +export const runLedgerDmkGenuineCheck = async ( + bridge: LedgerRelayApduBridge, + device: LedgerRelayDevice, + options?: { + ledgerWebSocketUrl?: string; + timeoutMs?: number; + } +): Promise => { + const timeoutMs = options?.timeoutMs ?? 5 * 60_000; + const builder = new DeviceManagementKitBuilder().addTransport( + createLedgerRelayTransportFactory({ bridge, device }) + ); + if (options?.ledgerWebSocketUrl) { + builder.addConfig({ webSocketUrl: options.ledgerWebSocketUrl }); + } + const dmk = builder.build(); + let sessionId: string | undefined; + try { + const discoveredDevice = await firstValueFrom( + dmk + .startDiscovering({ + transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, + }) + .pipe(take(1), timeout({ first: 5_000 })) + ); + sessionId = await dmk.connect({ + device: discoveredDevice, + sessionRefresherOptions: { isRefresherDisabled: true }, + }); + const action = dmk.executeDeviceAction({ + sessionId, + deviceAction: new GenuineCheckDeviceAction({ input: {} }), + }); + let deviceId: string | undefined; + + return await new Promise((resolve, reject) => { + let settled = false; + let subscription: { unsubscribe: () => void } | undefined; + const settle = (fn: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timer); + subscription?.unsubscribe(); + fn(); + }; + const timer = setTimeout(() => { + settle(() => { + action.cancel(); + reject(new Error('Ledger server Genuine Check timed out')); + }); + }, timeoutMs); + subscription = action.observable.subscribe({ + next: state => { + if (state.status === DeviceActionStatus.Pending) { + const intermediate = state.intermediateValue as + | { + deviceId?: Uint8Array; + requiredUserInteraction?: string; + } + | undefined; + if (intermediate?.deviceId instanceof Uint8Array && !deviceId) { + deviceId = Buffer.from(intermediate.deviceId).toString('hex'); + } + if (intermediate?.requiredUserInteraction) { + bridge.onInteraction?.(intermediate.requiredUserInteraction); + } + return; + } + if (state.status === DeviceActionStatus.Completed) { + const output = state.output as { isGenuine?: boolean }; + settle(() => { + if (output.isGenuine && !deviceId) { + reject( + new Error( + 'Ledger server Genuine Check succeeded without a physical-device DSID' + ) + ); + return; + } + resolve({ + isGenuine: output.isGenuine === true, + deviceId: output.isGenuine === true ? deviceId : undefined, + }); + }); + return; + } + if (state.status === DeviceActionStatus.Error) { + settle(() => reject(state.error)); + } + }, + error: error => settle(() => reject(error)), + }); + }); + } finally { + try { + await dmk.stopDiscovering(); + } catch { + // Discovery may have stopped after the first emitted device. + } + if (sessionId) { + try { + await dmk.disconnect({ sessionId }); + } catch { + // Best-effort cleanup; the result has already been decided. + } + } + dmk.close(); + } +}; diff --git a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts index dd6976ade..0d5ee7868 100644 --- a/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts +++ b/packages/hwk-ledger-adapter/src/connector/LedgerConnectorBase.ts @@ -247,6 +247,12 @@ export class LedgerConnectorBase implements IConnector { // --------------------------------------------------------------------------- private readonly _cancellers = new Map void>(); + /** + * A server-owned attestation relay temporarily owns the raw APDU stream. + * The stored function re-enables DMK's session refresher when ownership ends. + */ + private readonly _attestationBridgeReleasers = new Map void>(); + // --------------------------------------------------------------------------- // Per-session DMK state subscriptions // @@ -598,6 +604,7 @@ export class LedgerConnectorBase implements IConnector { } async disconnect(sessionId: string): Promise { + this._releaseAttestationBridge(sessionId); if (!this._deviceManager) return; const deviceId = this._deviceManager.getDeviceId(sessionId); @@ -676,6 +683,7 @@ export class LedgerConnectorBase implements IConnector { externalConnectId ); this._unwatchSessionState(sessionId); + this._releaseAttestationBridge(sessionId); this._signerManager?.invalidate(sessionId); this._cancellers.get(sessionId)?.({ code: HardwareErrorCode.DeviceDisconnected, @@ -938,6 +946,63 @@ export class LedgerConnectorBase implements IConnector { ctx.clearCanceller(sessionId); } } + case 'startDeviceAttestationApduBridge': { + if (this._attestationBridgeReleasers.has(sessionId)) { + throw new Error('Ledger device attestation APDU bridge is already active'); + } + const dmk = await ctx.getOrCreateDmk(); + const release = dmk.disableDeviceSessionRefresher({ + sessionId, + blockerId: 'onekey-ledger-attestation-relay', + }); + try { + const device = dmk.getConnectedDevice({ sessionId }); + this._attestationBridgeReleasers.set(sessionId, release); + return { + id: device.id, + modelId: device.modelId, + name: device.name, + connectionType: device.type, + }; + } catch (error) { + release(); + throw error; + } + } + case 'exchangeDeviceAttestationApdu': { + if (!this._attestationBridgeReleasers.has(sessionId)) { + throw new Error('Ledger device attestation APDU bridge is not active'); + } + const p = params as { apduHex?: unknown; timeoutMs?: unknown }; + if ( + typeof p.apduHex !== 'string' || + p.apduHex.length < 8 || + p.apduHex.length % 2 !== 0 || + !/^[0-9a-f]+$/i.test(p.apduHex) || + p.apduHex.length / 2 > 8 * 1024 + ) { + throw new Error('Invalid Ledger device attestation APDU'); + } + const requestedTimeout = + typeof p.timeoutMs === 'number' && Number.isFinite(p.timeoutMs) + ? p.timeoutMs + : 30_000; + const timeoutMs = Math.max(1_000, Math.min(requestedTimeout, 60_000)); + const dmk = await ctx.getOrCreateDmk(); + const response = await dmk.sendApdu({ + sessionId, + apdu: Uint8Array.from(Buffer.from(p.apduHex, 'hex')), + abortTimeout: timeoutMs, + triggersDisconnection: false, + }); + return { + dataHex: Buffer.from(response.data).toString('hex'), + statusCodeHex: Buffer.from(response.statusCode).toString('hex'), + }; + } + case 'stopDeviceAttestationApduBridge': + this._releaseAttestationBridge(sessionId); + return undefined; default: throw new Error(`LedgerConnector: unknown method "${method}"`); } @@ -1201,6 +1266,7 @@ export class LedgerConnectorBase implements IConnector { */ private _resetSignersAndSessions(): void { debugLog('[DMK] _resetSignersAndSessions called'); + this._releaseAllAttestationBridges(); this._signerManager?.clearAll(); this._signerManager = null; this._deviceAppsManager?.clearAll(); @@ -1211,6 +1277,7 @@ export class LedgerConnectorBase implements IConnector { private _resetAll(): void { debugLog('[DMK] _resetAll called'); + this._releaseAllAttestationBridges(); // Cancel any in-flight DeviceActions so their observables unsubscribe // and DMK's intent queues don't keep orphaned slots alive. for (const cancel of this._cancellers.values()) { @@ -1240,6 +1307,23 @@ export class LedgerConnectorBase implements IConnector { this._dmk = null; } + private _releaseAttestationBridge(sessionId: string): void { + const release = this._attestationBridgeReleasers.get(sessionId); + if (!release) return; + this._attestationBridgeReleasers.delete(sessionId); + try { + release(); + } catch { + // The DMK session may already be gone; ownership is still cleared. + } + } + + private _releaseAllAttestationBridges(): void { + for (const sessionId of [...this._attestationBridgeReleasers.keys()]) { + this._releaseAttestationBridge(sessionId); + } + } + // --------------------------------------------------------------------------- // Private -- Events // --------------------------------------------------------------------------- diff --git a/packages/hwk-ledger-adapter/tsup.config.ts b/packages/hwk-ledger-adapter/tsup.config.ts index 9903ebca5..7217d190e 100644 --- a/packages/hwk-ledger-adapter/tsup.config.ts +++ b/packages/hwk-ledger-adapter/tsup.config.ts @@ -1,7 +1,10 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/index.ts'], + entry: { + index: 'src/index.ts', + 'attestation-relay': 'src/attestationRelay/index.ts', + }, format: ['cjs', 'esm'], dts: true, clean: true, @@ -18,5 +21,6 @@ export default defineConfig({ 'react-native', 'react-native-ble-plx', 'rxjs', + 'ws', ], }); diff --git a/scripts/monitor-config.json b/scripts/monitor-config.json index c65064022..6450c0504 100644 --- a/scripts/monitor-config.json +++ b/scripts/monitor-config.json @@ -29,7 +29,8 @@ "hwk-trezor-connector-electron-ble" ], "specialProjects": { - "hd-web-sdk": ["dist", "build"] + "hd-web-sdk": ["dist", "build"], + "hwk-ledger-adapter": ["dist", "package.json"] }, "ignoredPaths": [ "node_modules", From 214eed9579dbd0705d399afdefe395ac92016e84 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Tue, 28 Jul 2026 10:28:51 +0800 Subject: [PATCH 08/12] refactor: move Ledger relay out of SDK --- HANDOFF-device-attestation.md | 27 +- docs/device-attestation-voucher-backend.md | 34 +- packages/hwk-ledger-adapter/package.json | 16 +- .../LedgerAttestationRelayServer.ts | 353 ------------------ .../LedgerAttestationRelayServer.test.ts | 149 -------- .../__tests__/protocol.test.ts | 35 -- .../__tests__/relayTransport.test.ts | 45 --- .../src/attestationRelay/index.ts | 25 -- .../src/attestationRelay/protocol.ts | 184 --------- .../src/attestationRelay/relayTransport.ts | 87 ----- .../runLedgerDmkGenuineCheck.ts | 129 ------- packages/hwk-ledger-adapter/tsup.config.ts | 6 +- scripts/monitor-config.json | 3 +- 13 files changed, 30 insertions(+), 1063 deletions(-) delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/index.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts delete mode 100644 packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index 192f5155a..05243b5e2 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -7,7 +7,7 @@ > **重要纠正**:早期 T3W1 真机结果只覆盖 Optiga + Tropic,不能代表当前要求的 > Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。Ledger 已改成 > “本地 OneKey 服务端拥有 DMK 状态机、App 只转发物理设备 APDU”;它不是离线证明, -> 但服务端所有权与生产方案一致,后续可把同一套 TypeScript 搬到远端。 +> 本地实现只用于 Monorepo 联调,生产后端按本文协议独立实现。 --- @@ -108,11 +108,11 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 **真机结果**(Nano X):`Verified ✅`,`deviceId=818706ff78239d1ab642df69c1d1fdd48acfc59ba282f4139383cfee31adfcdc`(走 genuine-check backend)。`attestationPubKey` 字段的真机验证**待做**(清缓存重启 Desktop 后看 Ledger 结果多一行 `attestationPubKey: 04...`)。 -#### 3.2.1 当前“服务端拥有 DMK”实现 +#### 3.2.1 当前 Monorepo 本地“服务端拥有 DMK”实现 现在 Claim 不再调用客户端 `verifyDeviceAuthenticity()` 后相信它返回的布尔值。真实路径是: -1. Desktop background 启动 `LedgerAttestationRelayServer`,只监听 +1. Desktop background 启动 `LedgerLocalAttestationServer`,只监听 `127.0.0.1`,创建 32-byte 一次性 token 和 5 分钟 ticket; 2. SDK 的 `LedgerAdapter.runDeviceAttestationApduBridge()` 独占当前已连接 Ledger session,并暂停 DMK session refresher; @@ -124,23 +124,24 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 WSS 发给客户端的 `result` 仅用于 UX,客户端不能提交它来发券; 6. success、failure、断线或取消都会恢复 refresher 并释放设备队列。 -代码: +代码都在 App Monorepo: -- `packages/hwk-ledger-adapter/src/attestationRelay/` - - `LedgerAttestationRelayServer.ts`:一次性 ticket/WSS 会话; +- `packages/kit-bg/src/services/ServiceThirdPartyHardware/ledgerLocalAttestationServer/` + - `LedgerLocalAttestationServer.ts`:一次性 ticket/WSS 会话; - `relayTransport.ts`:远端 APDU → DMK `TransportFactory`; - `runLedgerDmkGenuineCheck.ts`:服务端官方 DMK action; - `protocol.ts`:严格消息解析、大小/次数/顺序限制。 -- `LedgerConnectorBase.ts`: +- SDK 只保留 `LedgerConnectorBase.ts` 的 `start/exchange/stopDeviceAttestationApduBridge`; -- `LedgerAdapter.ts`:`runDeviceAttestationApduBridge()` 在整个 relay 生命周期独占 +- SDK `LedgerAdapter.ts`:`runDeviceAttestationApduBridge()` 在整个 relay 生命周期独占 DeviceJobQueue; - App Desktop: `ServiceThirdPartyHardware/ledgerLocalAttestationBridge.desktop.ts`。 -本地与远端的业务代码不变。上线时把 `LedgerAttestationRelayServer.listen()` 放进 -后端进程,用 API 创建 session,把 `ws://127.0.0.1` ticket 换成带登录态/短期 token 的 -`wss://attestation.`;App 的 APDU client 与协议无需改写。 +这套 Node 服务不从 SDK 发布,也不作为生产后端包。后端团队按 +`docs/device-attestation-voucher-backend.md` 的状态机和 WSS contract 独立实现; +App 的 APDU client 后续只需把本地 `ws://127.0.0.1` ticket 换成后端返回的 +`wss://attestation.`。 ### 3.3 Review 修复状态(Fable 5 审查) @@ -182,8 +183,8 @@ yarn workspace @onekeyhq/desktop dev **构建环境**:`tsup` 曾部分缺失,`yarn install` 可恢复(本会话已跑过一次)。 > 当前使用 `yarn debug:watch` 同步本地 SDK,不发 alpha 包。 -> `scripts/monitor-config.json` 已让 `hwk-ledger-adapter` 同步 `dist` 和 -> `package.json`,因为 `attestation-relay` 是新的 Node-only package subpath。 +> `hwk-ledger-adapter` 只需同步 `dist`;本地 relay 已归属 App Monorepo,不需要同步 +> SDK `package.json`。 --- diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md index 38129d00d..d04b76a72 100644 --- a/docs/device-attestation-voucher-backend.md +++ b/docs/device-attestation-voucher-backend.md @@ -277,25 +277,16 @@ JWS/COSE receipt。单纯透明代理 Ledger WSS、由客户端计算 `isGenuine 证明服务端可独立观察并绑定最终 verdict,否则仍然不够;客户端 `{ verified: true, deviceId }` 永远不能作为发券证据。 -#### 4.3.1 可直接部署的 DMK 状态机 - -当前 SDK 已提供 Node-only subpath: - -```ts -import { - LedgerAttestationRelayServer, -} from '@onekeyfe/hwk-ledger-adapter/attestation-relay'; - -const relay = await LedgerAttestationRelayServer.listen({ - host: '127.0.0.1', - port: 0, -}); -const ticket = relay.createSession(); -// ticket.webSocketUrl 给 App;ticket.result 只留在可信服务端。 -const authoritativeResult = await ticket.result; -``` +#### 4.3.1 后端需要独立实现的 DMK 状态机 + +App Monorepo 中有一份只用于本地联调的参考实现: + +`packages/kit-bg/src/services/ServiceThirdPartyHardware/ledgerLocalAttestationServer/` + +它不属于 Hardware SDK、不通过 npm subpath 发布,也不是生产后端交付物。后端团队 +需要使用自己的服务框架、鉴权、存储和部署方式独立实现下面的状态机与 WSS contract。 -`LedgerAttestationRelayServer` 内部运行官方 +本地 `LedgerLocalAttestationServer` 运行官方 `GenuineCheckDeviceAction`,不是模拟 `isGenuine`。它通过自定义 DMK `TransportFactory` 把每条 APDU 交给 WSS 客户端,客户端再用已连接的 USB/BLE session 发给物理 Ledger。服务端同时直连 Ledger 官方 HSM endpoint,因此验证时必须 @@ -334,9 +325,10 @@ Server -> App: interaction / result / error - `isGenuine=false` 时丢弃任何 `deviceId`; - 只有服务端保存的 Promise/数据库 session 能完成 claim,客户端 `result` 不具备授权力。 -生产化时保持 `protocol.ts`、`relayTransport.ts` 和 -`runLedgerDmkGenuineCheck.ts` 不变;将内存 ticket store 替换为 Redis/数据库,把本地 -`ws://` 升级为 TLS `wss://`,接入 OneKey 登录态、限流、审计和多实例 sticky routing。 +后端实现时可参考本地 `protocol.ts`、`relayTransport.ts` 和 +`runLedgerDmkGenuineCheck.ts` 的行为,但不要求复用其代码。生产必须将内存 ticket +store 替换为 Redis/数据库,把本地 `ws://` 升级为 TLS `wss://`,接入 OneKey 登录态、 +限流、审计和多实例 sticky routing。 同一 attestation session 的 WSS 与 DMK runner 必须由同一 worker 拥有,或用严格有序的 消息队列转发。 diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index fd7c11d24..a095af3e5 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -17,16 +17,6 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" } - }, - "./attestation-relay": { - "import": { - "types": "./dist/attestation-relay.d.mts", - "default": "./dist/attestation-relay.mjs" - }, - "require": { - "types": "./dist/attestation-relay.d.ts", - "default": "./dist/attestation-relay.js" - } } }, "files": [ @@ -62,13 +52,9 @@ "@ledgerhq/hw-transport": "^6.34.1", "@noble/hashes": "^1.3.3", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1", - "purify-ts": "2.1.0", - "rxjs": "7.8.2", - "ws": "^7.5.10" + "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" }, "devDependencies": { - "@types/ws": "^8.5.5", "rimraf": "^5.0.0", "tsup": "^8.0.0", "typescript": "^5.4.0" diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts b/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts deleted file mode 100644 index 37cf46f18..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/LedgerAttestationRelayServer.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { randomBytes, randomUUID } from 'node:crypto'; -import { createServer } from 'node:http'; - -import WebSocket from 'ws'; - -import { - DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, - LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, - MAX_LEDGER_RELAY_APDU_BYTES, - MAX_LEDGER_RELAY_APDU_EXCHANGES, - parseLedgerRelayClientMessage, -} from './protocol'; -import { runLedgerDmkGenuineCheck } from './runLedgerDmkGenuineCheck'; - -import type { AddressInfo } from 'node:net'; -import type { - LedgerRelayClientMessage, - LedgerRelayDevice, - LedgerRelayServerMessage, -} from './protocol'; -import type { - LedgerRelayApduBridge, - LedgerRelayApduResponse, -} from './relayTransport'; -import type { LedgerServerGenuineCheckResult } from './runLedgerDmkGenuineCheck'; - -type RelaySession = { - expiresAt: number; - consumed: boolean; - expiryTimer: ReturnType; - resolve: (result: LedgerServerGenuineCheckResult) => void; - reject: (error: Error) => void; -}; - -type PendingApdu = { - requestId: string; - resolve: (response: LedgerRelayApduResponse) => void; - reject: (error: Error) => void; - timer: ReturnType; -}; - -export type LedgerAttestationRelayTicket = { - webSocketUrl: string; - expiresAt: number; - result: Promise; -}; - -export type RunLedgerServerGenuineCheck = ( - bridge: LedgerRelayApduBridge, - device: LedgerRelayDevice -) => Promise; - -const sendMessage = (socket: WebSocket, message: LedgerRelayServerMessage) => { - socket.send(JSON.stringify(message)); -}; - -const asError = (error: unknown): Error => - error instanceof Error ? error : new Error(String(error)); - -export class LedgerAttestationRelayServer { - private readonly sessions = new Map(); - - private constructor( - private readonly httpServer: ReturnType, - private readonly webSocketServer: WebSocket.Server, - private readonly baseUrl: string, - private readonly sessionTtlMs: number, - private readonly runGenuineCheck: RunLedgerServerGenuineCheck - ) { - webSocketServer.on('connection', (socket, request) => { - this.handleConnection(socket, request.url ?? '/'); - }); - } - - static async listen(options?: { - host?: string; - port?: number; - sessionTtlMs?: number; - publicWebSocketBaseUrl?: string; - runGenuineCheck?: RunLedgerServerGenuineCheck; - }): Promise { - const host = options?.host ?? '127.0.0.1'; - if (options?.publicWebSocketBaseUrl) { - let parsedPublicUrl: URL; - try { - parsedPublicUrl = new URL(options.publicWebSocketBaseUrl); - } catch { - throw new Error('Ledger relay public WebSocket base URL is invalid'); - } - if ( - (parsedPublicUrl.protocol !== 'ws:' && - parsedPublicUrl.protocol !== 'wss:') || - parsedPublicUrl.username || - parsedPublicUrl.password || - parsedPublicUrl.search || - parsedPublicUrl.hash - ) { - throw new Error('Ledger relay public WebSocket base URL is invalid'); - } - } - const httpServer = createServer(); - const webSocketServer = new WebSocket.Server({ - server: httpServer, - maxPayload: MAX_LEDGER_RELAY_APDU_BYTES * 2 + 4_096, - }); - await new Promise((resolve, reject) => { - httpServer.once('error', reject); - httpServer.listen(options?.port ?? 0, host, () => { - httpServer.off('error', reject); - resolve(); - }); - }); - const address = httpServer.address() as AddressInfo; - const publicWebSocketBaseUrl = - options?.publicWebSocketBaseUrl ?? `ws://${host}:${address.port}`; - return new LedgerAttestationRelayServer( - httpServer, - webSocketServer, - publicWebSocketBaseUrl.replace(/\/+$/, ''), - options?.sessionTtlMs ?? DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, - options?.runGenuineCheck ?? runLedgerDmkGenuineCheck - ); - } - - createSession(): LedgerAttestationRelayTicket { - const token = randomBytes(32).toString('hex'); - const expiresAt = Date.now() + this.sessionTtlMs; - let resolve!: (result: LedgerServerGenuineCheckResult) => void; - let reject!: (error: Error) => void; - const result = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - const expiryTimer = setTimeout(() => { - const session = this.sessions.get(token); - if (!session || session.consumed) return; - this.sessions.delete(token); - session.reject(new Error('Ledger attestation relay session expired')); - }, this.sessionTtlMs); - expiryTimer.unref?.(); - this.sessions.set(token, { - expiresAt, - consumed: false, - expiryTimer, - resolve, - reject, - }); - return { - webSocketUrl: `${this.baseUrl}/v1/ledger/attestation/${token}`, - expiresAt, - result, - }; - } - - private handleConnection(socket: WebSocket, rawUrl: string): void { - let parsed: URL; - try { - parsed = new URL(rawUrl, this.baseUrl); - } catch { - socket.close(4400, 'invalid-url'); - return; - } - if (parsed.search || parsed.hash) { - socket.close(4400, 'invalid-url'); - return; - } - const match = parsed.pathname.match( - /^\/v1\/ledger\/attestation\/([0-9a-f]{64})$/ - ); - const token = match?.[1]; - if (!token) { - socket.close(4404, 'session-not-found'); - return; - } - const session = this.sessions.get(token); - if ( - !session || - session.consumed || - session.expiresAt <= Date.now() - ) { - socket.close(4404, 'session-not-found'); - return; - } - session.consumed = true; - clearTimeout(session.expiryTimer); - this.sessions.delete(token); - let helloReceived = false; - let finished = false; - let exchangeCount = 0; - let pendingApdu: PendingApdu | undefined; - - const fail = (error: unknown, closeCode = 4400) => { - if (finished) return; - finished = true; - const normalized = asError(error); - if (pendingApdu) { - clearTimeout(pendingApdu.timer); - pendingApdu.reject(normalized); - pendingApdu = undefined; - } - session.reject(normalized); - if (socket.readyState === WebSocket.OPEN) { - sendMessage(socket, { - type: 'error', - code: 'ledger_attestation_failed', - message: normalized.message, - }); - socket.close(closeCode, 'attestation-failed'); - } - }; - - const bridge: LedgerRelayApduBridge = { - exchangeApdu: (apdu, timeoutMs = 30_000) => { - if (finished || socket.readyState !== WebSocket.OPEN) { - return Promise.reject(new Error('Ledger relay client disconnected')); - } - if (pendingApdu) { - return Promise.reject( - new Error('Ledger relay permits only one outstanding APDU') - ); - } - if (apdu.byteLength > MAX_LEDGER_RELAY_APDU_BYTES) { - return Promise.reject(new Error('Ledger relay APDU is too large')); - } - exchangeCount += 1; - if (exchangeCount > MAX_LEDGER_RELAY_APDU_EXCHANGES) { - return Promise.reject( - new Error('Ledger relay APDU exchange limit exceeded') - ); - } - const requestId = randomUUID(); - return new Promise((resolve, reject) => { - const boundedTimeout = Math.max(1_000, Math.min(timeoutMs, 60_000)); - const timer = setTimeout(() => { - pendingApdu = undefined; - reject(new Error('Ledger relay APDU timed out')); - }, boundedTimeout); - pendingApdu = { requestId, resolve, reject, timer }; - sendMessage(socket, { - type: 'apdu-request', - requestId, - apduHex: Buffer.from(apdu).toString('hex'), - timeoutMs: boundedTimeout, - }); - }); - }, - onInteraction: requiredUserInteraction => { - if (!finished && socket.readyState === WebSocket.OPEN) { - sendMessage(socket, { - type: 'interaction', - requiredUserInteraction, - }); - } - }, - }; - - const handleClientMessage = (message: LedgerRelayClientMessage) => { - if (!helloReceived) { - if (message.type !== 'hello') { - throw new Error('Ledger relay expected hello first'); - } - helloReceived = true; - sendMessage(socket, { - type: 'ready', - protocolVersion: LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, - }); - void this.runGenuineCheck(bridge, message.device).then( - result => { - if (finished) return; - if ( - result.isGenuine === true && - (!result.deviceId || !/^[0-9a-f]{64}$/i.test(result.deviceId)) - ) { - fail( - new Error( - 'Ledger server Genuine Check succeeded without a valid physical-device DSID' - ) - ); - return; - } - finished = true; - const authoritativeResult: LedgerServerGenuineCheckResult = - result.isGenuine === true - ? result - : { isGenuine: false, deviceId: undefined }; - session.resolve(authoritativeResult); - sendMessage(socket, { - type: 'result', - isGenuine: authoritativeResult.isGenuine, - deviceId: authoritativeResult.deviceId, - }); - socket.close(1000, 'complete'); - }, - error => fail(error) - ); - return; - } - if (message.type !== 'apdu-response' && message.type !== 'apdu-error') { - throw new Error('Unexpected Ledger relay message'); - } - if (!pendingApdu || pendingApdu.requestId !== message.requestId) { - throw new Error('Ledger relay APDU response is out of order'); - } - const pending = pendingApdu; - pendingApdu = undefined; - clearTimeout(pending.timer); - if (message.type === 'apdu-error') { - pending.reject(new Error(message.message)); - } else { - pending.resolve({ - data: Uint8Array.from(Buffer.from(message.dataHex, 'hex')), - statusCode: Uint8Array.from( - Buffer.from(message.statusCodeHex, 'hex') - ), - }); - } - }; - - socket.on('message', raw => { - try { - handleClientMessage(parseLedgerRelayClientMessage(raw.toString())); - } catch (error) { - fail(error); - } - }); - socket.on('close', () => { - if (!finished) { - fail(new Error('Ledger relay client disconnected'), 4408); - } - }); - socket.on('error', error => fail(error, 1011)); - } - - async close(): Promise { - const error = new Error('Ledger attestation relay server closed'); - for (const session of this.sessions.values()) { - clearTimeout(session.expiryTimer); - if (!session.consumed) { - session.reject(error); - } - } - this.sessions.clear(); - for (const socket of this.webSocketServer.clients) { - socket.close(1001, 'server-closed'); - } - await new Promise(resolve => { - this.webSocketServer.close(() => resolve()); - }); - await new Promise(resolve => { - this.httpServer.close(() => resolve()); - }); - } -} diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts deleted file mode 100644 index de0ddde2b..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/LedgerAttestationRelayServer.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import WebSocket from 'ws'; - -import { LedgerAttestationRelayServer } from '../LedgerAttestationRelayServer'; - -import type { LedgerRelayClientMessage, LedgerRelayServerMessage } from '../protocol'; - -const waitForOpen = (socket: WebSocket) => - new Promise((resolve, reject) => { - socket.once('open', resolve); - socket.once('error', reject); - }); - -describe('LedgerAttestationRelayServer', () => { - let server: LedgerAttestationRelayServer | undefined; - - afterEach(async () => { - await server?.close(); - server = undefined; - }); - - it('owns the DMK result while the client only forwards APDUs', async () => { - const runGenuineCheck = jest.fn(async bridge => { - const response = await bridge.exchangeApdu( - Uint8Array.from([0xe0, 0x01, 0x00, 0x00, 0x00]), - 2_000 - ); - expect(Buffer.from(response.data).toString('hex')).toBe('abcd'); - expect(Buffer.from(response.statusCode).toString('hex')).toBe('9000'); - return { - isGenuine: true, - deviceId: 'ab'.repeat(32), - }; - }); - server = await LedgerAttestationRelayServer.listen({ - host: '127.0.0.1', - port: 0, - runGenuineCheck, - }); - const ticket = server.createSession(); - const socket = new WebSocket(ticket.webSocketUrl); - await waitForOpen(socket); - - socket.on('message', raw => { - const message = JSON.parse(raw.toString()) as LedgerRelayServerMessage; - if (message.type === 'apdu-request') { - socket.send( - JSON.stringify({ - type: 'apdu-response', - requestId: message.requestId, - dataHex: 'abcd', - statusCodeHex: '9000', - } satisfies LedgerRelayClientMessage) - ); - } - }); - socket.send( - JSON.stringify({ - type: 'hello', - protocolVersion: 1, - device: { - id: 'local-ledger', - modelId: 'nanoX', - name: 'Ledger Nano X', - }, - } satisfies LedgerRelayClientMessage) - ); - - await expect(ticket.result).resolves.toEqual({ - isGenuine: true, - deviceId: 'ab'.repeat(32), - }); - expect(runGenuineCheck).toHaveBeenCalledTimes(1); - expect(runGenuineCheck.mock.calls[0][1]).toMatchObject({ - id: 'local-ledger', - modelId: 'nanoX', - name: 'Ledger Nano X', - }); - }); - - it('rejects reuse of a consumed single-use session token', async () => { - server = await LedgerAttestationRelayServer.listen({ - host: '127.0.0.1', - port: 0, - runGenuineCheck: async () => ({ - isGenuine: true, - deviceId: 'cd'.repeat(32), - }), - }); - const ticket = server.createSession(); - const first = new WebSocket(ticket.webSocketUrl); - await waitForOpen(first); - first.send( - JSON.stringify({ - type: 'hello', - protocolVersion: 1, - device: { id: 'ledger', modelId: 'nanoX' }, - } satisfies LedgerRelayClientMessage) - ); - await ticket.result; - - const second = new WebSocket(ticket.webSocketUrl); - const closeCode = await new Promise((resolve, reject) => { - second.once('close', resolve); - second.once('error', reject); - }); - expect(closeCode).toBe(4404); - }); - - it('supports a TLS reverse-proxy public WSS base for production deployment', async () => { - server = await LedgerAttestationRelayServer.listen({ - host: '127.0.0.1', - port: 0, - publicWebSocketBaseUrl: 'wss://attestation.onekey.test/', - runGenuineCheck: async () => ({ - isGenuine: true, - deviceId: 'ef'.repeat(32), - }), - }); - - const ticket = server.createSession(); - void ticket.result.catch(() => undefined); - expect(ticket.webSocketUrl).toMatch( - /^wss:\/\/attestation\.onekey\.test\/v1\/ledger\/attestation\/[0-9a-f]{64}$/ - ); - }); - - it('fails closed if a genuine verdict has no valid physical-device DSID', async () => { - server = await LedgerAttestationRelayServer.listen({ - host: '127.0.0.1', - port: 0, - runGenuineCheck: async () => ({ - isGenuine: true, - deviceId: 'client-controlled-value', - }), - }); - const ticket = server.createSession(); - const socket = new WebSocket(ticket.webSocketUrl); - await waitForOpen(socket); - socket.send( - JSON.stringify({ - type: 'hello', - protocolVersion: 1, - device: { id: 'ledger', modelId: 'nanoX' }, - } satisfies LedgerRelayClientMessage) - ); - - await expect(ticket.result).rejects.toThrow('valid physical-device DSID'); - }); -}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts deleted file mode 100644 index 592749d66..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/protocol.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - MAX_LEDGER_RELAY_APDU_BYTES, - parseLedgerRelayClientMessage, -} from '../protocol'; - -describe('Ledger attestation relay protocol', () => { - it('rejects oversized APDU responses before allocating relay state', () => { - expect(() => - parseLedgerRelayClientMessage( - JSON.stringify({ - type: 'apdu-response', - requestId: 'request-1', - dataHex: 'aa'.repeat(MAX_LEDGER_RELAY_APDU_BYTES + 1), - statusCodeHex: '9000', - }) - ) - ).toThrow('too large'); - }); - - it('rejects malformed status words and unknown messages', () => { - expect(() => - parseLedgerRelayClientMessage( - JSON.stringify({ - type: 'apdu-response', - requestId: 'request-1', - dataHex: '', - statusCodeHex: '90', - }) - ) - ).toThrow('statusCodeHex'); - expect(() => parseLedgerRelayClientMessage('{"type":"verified","verified":true}')).toThrow( - 'Unsupported' - ); - }); -}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts b/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts deleted file mode 100644 index 3af6b23a2..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/__tests__/relayTransport.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { firstValueFrom } from 'rxjs'; - -import { createLedgerRelayTransportFactory } from '../relayTransport'; - -describe('createLedgerRelayTransportFactory', () => { - it('adapts a remote APDU bridge to the Ledger DMK TransportFactory contract', async () => { - const bridge = { - exchangeApdu: jest.fn(async () => ({ - data: Uint8Array.from([0x12, 0x34]), - statusCode: Uint8Array.from([0x90, 0x00]), - })), - }; - const transport = createLedgerRelayTransportFactory({ - bridge, - device: { id: 'relay-device', modelId: 'nanoX', name: 'Ledger Nano X' }, - })({ - deviceModelDataSource: { - getDeviceModel: jest.fn().mockReturnValue({ id: 'nanoX' }), - }, - } as any); - - await expect(firstValueFrom(transport.startDiscovering())).resolves.toMatchObject({ - id: 'relay-device', - name: 'Ledger Nano X', - transport: 'ONEKEY_LEDGER_ATTESTATION_RELAY', - }); - const connected = await transport.connect({ - deviceId: 'relay-device', - onDisconnect: jest.fn(), - }); - expect(connected.isRight()).toBe(true); - const device = connected.extract() as { - sendApdu: (apdu: Uint8Array) => Promise<{ - isRight: () => boolean; - extract: () => { data: Uint8Array; statusCode: Uint8Array }; - }>; - }; - const response = await device.sendApdu(Uint8Array.from([0xe0, 0x01])); - expect(response.isRight()).toBe(true); - expect(response.extract()).toMatchObject({ - data: Uint8Array.from([0x12, 0x34]), - statusCode: Uint8Array.from([0x90, 0x00]), - }); - }); -}); diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/index.ts b/packages/hwk-ledger-adapter/src/attestationRelay/index.ts deleted file mode 100644 index f7d0c8b6d..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export { - LedgerAttestationRelayServer, - type LedgerAttestationRelayTicket, - type RunLedgerServerGenuineCheck, -} from './LedgerAttestationRelayServer'; -export { - createLedgerRelayTransportFactory, - LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - type LedgerRelayApduBridge, - type LedgerRelayApduResponse, -} from './relayTransport'; -export { - DEFAULT_LEDGER_RELAY_SESSION_TTL_MS, - LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, - MAX_LEDGER_RELAY_APDU_BYTES, - MAX_LEDGER_RELAY_APDU_EXCHANGES, - parseLedgerRelayClientMessage, - type LedgerRelayClientMessage, - type LedgerRelayDevice, - type LedgerRelayServerMessage, -} from './protocol'; -export { - runLedgerDmkGenuineCheck, - type LedgerServerGenuineCheckResult, -} from './runLedgerDmkGenuineCheck'; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts b/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts deleted file mode 100644 index d3b2e07c8..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/protocol.ts +++ /dev/null @@ -1,184 +0,0 @@ -export const LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION = 1 as const; -export const MAX_LEDGER_RELAY_APDU_BYTES = 8 * 1024; -export const MAX_LEDGER_RELAY_APDU_EXCHANGES = 256; -export const DEFAULT_LEDGER_RELAY_SESSION_TTL_MS = 5 * 60_000; - -const REQUEST_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/; -const HEX_PATTERN = /^[0-9a-f]*$/i; -const LEDGER_MODEL_IDS = new Set(['nanoS', 'nanoSP', 'nanoX', 'stax', 'flex', 'apexp']); - -export type LedgerRelayDevice = { - id: string; - modelId: 'nanoS' | 'nanoSP' | 'nanoX' | 'stax' | 'flex' | 'apexp'; - name?: string; - connectionType?: 'USB' | 'BLE'; -}; - -export type LedgerRelayHelloMessage = { - type: 'hello'; - protocolVersion: typeof LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION; - device: LedgerRelayDevice; -}; - -export type LedgerRelayApduResponseMessage = { - type: 'apdu-response'; - requestId: string; - dataHex: string; - statusCodeHex: string; -}; - -export type LedgerRelayApduErrorMessage = { - type: 'apdu-error'; - requestId: string; - message: string; -}; - -export type LedgerRelayClientMessage = - | LedgerRelayHelloMessage - | LedgerRelayApduResponseMessage - | LedgerRelayApduErrorMessage; - -export type LedgerRelayServerMessage = - | { - type: 'ready'; - protocolVersion: typeof LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION; - } - | { - type: 'apdu-request'; - requestId: string; - apduHex: string; - timeoutMs: number; - } - | { - type: 'interaction'; - requiredUserInteraction: string; - } - | { - type: 'result'; - isGenuine: boolean; - deviceId?: string; - } - | { - type: 'error'; - code: string; - message: string; - }; - -const assertHex = ({ - value, - field, - exactBytes, - maxBytes, -}: { - value: unknown; - field: string; - exactBytes?: number; - maxBytes?: number; -}): string => { - if ( - typeof value !== 'string' || - value.length % 2 !== 0 || - !HEX_PATTERN.test(value) || - (exactBytes !== undefined && value.length !== exactBytes * 2) - ) { - throw new Error(`Invalid Ledger relay ${field}`); - } - if (maxBytes !== undefined && value.length / 2 > maxBytes) { - throw new Error(`Ledger relay ${field} is too large`); - } - return value.toLowerCase(); -}; - -const assertRequestId = (value: unknown): string => { - if (typeof value !== 'string' || !REQUEST_ID_PATTERN.test(value)) { - throw new Error('Invalid Ledger relay requestId'); - } - return value; -}; - -const parseDevice = (value: unknown): LedgerRelayDevice => { - if (!value || typeof value !== 'object') { - throw new Error('Invalid Ledger relay device'); - } - const device = value as Record; - if ( - typeof device.id !== 'string' || - device.id.length < 1 || - device.id.length > 128 || - typeof device.modelId !== 'string' || - !LEDGER_MODEL_IDS.has(device.modelId) - ) { - throw new Error('Invalid Ledger relay device metadata'); - } - if ( - device.name !== undefined && - (typeof device.name !== 'string' || device.name.length > 128) - ) { - throw new Error('Invalid Ledger relay device name'); - } - if ( - device.connectionType !== undefined && - device.connectionType !== 'USB' && - device.connectionType !== 'BLE' - ) { - throw new Error('Invalid Ledger relay connection type'); - } - return device as LedgerRelayDevice; -}; - -export const parseLedgerRelayClientMessage = (raw: string): LedgerRelayClientMessage => { - if (Buffer.byteLength(raw, 'utf8') > MAX_LEDGER_RELAY_APDU_BYTES * 2 + 4_096) { - throw new Error('Ledger relay message is too large'); - } - let value: unknown; - try { - value = JSON.parse(raw); - } catch { - throw new Error('Invalid Ledger relay JSON'); - } - if (!value || typeof value !== 'object') { - throw new Error('Invalid Ledger relay message'); - } - const message = value as Record; - if (message.type === 'hello') { - if (message.protocolVersion !== LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION) { - throw new Error('Unsupported Ledger relay protocol version'); - } - return { - type: 'hello', - protocolVersion: LEDGER_ATTESTATION_RELAY_PROTOCOL_VERSION, - device: parseDevice(message.device), - }; - } - if (message.type === 'apdu-response') { - return { - type: 'apdu-response', - requestId: assertRequestId(message.requestId), - dataHex: assertHex({ - value: message.dataHex, - field: 'dataHex', - maxBytes: MAX_LEDGER_RELAY_APDU_BYTES, - }), - statusCodeHex: assertHex({ - value: message.statusCodeHex, - field: 'statusCodeHex', - exactBytes: 2, - }), - }; - } - if (message.type === 'apdu-error') { - if ( - typeof message.message !== 'string' || - message.message.length < 1 || - message.message.length > 512 - ) { - throw new Error('Invalid Ledger relay APDU error'); - } - return { - type: 'apdu-error', - requestId: assertRequestId(message.requestId), - message: message.message, - }; - } - throw new Error('Unsupported Ledger relay message'); -}; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts b/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts deleted file mode 100644 index 4d99e10ab..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/relayTransport.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - ApduResponse, - GeneralDmkError, - TransportConnectedDevice, - UnknownDeviceError, -} from '@ledgerhq/device-management-kit'; -import { Left, Right } from 'purify-ts'; -import { of } from 'rxjs'; - -import type { - DeviceModelId, - Transport, - TransportFactory, -} from '@ledgerhq/device-management-kit'; - -import type { LedgerRelayDevice } from './protocol'; - -export const LEDGER_ATTESTATION_RELAY_TRANSPORT_ID = - 'ONEKEY_LEDGER_ATTESTATION_RELAY'; - -export type LedgerRelayApduResponse = { - data: Uint8Array; - statusCode: Uint8Array; -}; - -export type LedgerRelayApduBridge = { - exchangeApdu: ( - apdu: Uint8Array, - timeoutMs?: number - ) => Promise; - onInteraction?: (requiredUserInteraction: string) => void; -}; - -export const createLedgerRelayTransportFactory = ({ - bridge, - device, -}: { - bridge: LedgerRelayApduBridge; - device: LedgerRelayDevice; -}): TransportFactory => args => { - const deviceModel = args.deviceModelDataSource.getDeviceModel({ - id: device.modelId as DeviceModelId, - }); - const discoveredDevice = { - id: device.id, - deviceModel, - transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - name: device.name, - }; - let connectedDevice: TransportConnectedDevice | undefined; - - const transport: Transport = { - getIdentifier: () => LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - isSupported: () => true, - startDiscovering: () => of(discoveredDevice), - stopDiscovering: () => undefined, - listenToAvailableDevices: () => of([discoveredDevice]), - connect: async ({ deviceId }) => { - if (deviceId !== device.id) { - return Left(new UnknownDeviceError()); - } - if (!connectedDevice) { - connectedDevice = new TransportConnectedDevice({ - id: device.id, - deviceModel, - type: device.connectionType ?? 'USB', - transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - name: device.name, - sendApdu: async (apdu, _triggersDisconnection, abortTimeout) => { - try { - const response = await bridge.exchangeApdu(apdu, abortTimeout); - return Right(new ApduResponse(response)); - } catch (error) { - return Left(new GeneralDmkError(error)); - } - }, - }); - } - return Right(connectedDevice); - }, - disconnect: async () => { - connectedDevice = undefined; - return Right(undefined); - }, - }; - return transport; -}; diff --git a/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts b/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts deleted file mode 100644 index 6ce1126a1..000000000 --- a/packages/hwk-ledger-adapter/src/attestationRelay/runLedgerDmkGenuineCheck.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - DeviceActionStatus, - DeviceManagementKitBuilder, - GenuineCheckDeviceAction, -} from '@ledgerhq/device-management-kit'; -import { firstValueFrom, take, timeout } from 'rxjs'; - -import { - LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - createLedgerRelayTransportFactory, -} from './relayTransport'; - -import type { LedgerRelayApduBridge } from './relayTransport'; -import type { LedgerRelayDevice } from './protocol'; - -export type LedgerServerGenuineCheckResult = { - isGenuine: boolean; - deviceId?: string; -}; - -export const runLedgerDmkGenuineCheck = async ( - bridge: LedgerRelayApduBridge, - device: LedgerRelayDevice, - options?: { - ledgerWebSocketUrl?: string; - timeoutMs?: number; - } -): Promise => { - const timeoutMs = options?.timeoutMs ?? 5 * 60_000; - const builder = new DeviceManagementKitBuilder().addTransport( - createLedgerRelayTransportFactory({ bridge, device }) - ); - if (options?.ledgerWebSocketUrl) { - builder.addConfig({ webSocketUrl: options.ledgerWebSocketUrl }); - } - const dmk = builder.build(); - let sessionId: string | undefined; - try { - const discoveredDevice = await firstValueFrom( - dmk - .startDiscovering({ - transport: LEDGER_ATTESTATION_RELAY_TRANSPORT_ID, - }) - .pipe(take(1), timeout({ first: 5_000 })) - ); - sessionId = await dmk.connect({ - device: discoveredDevice, - sessionRefresherOptions: { isRefresherDisabled: true }, - }); - const action = dmk.executeDeviceAction({ - sessionId, - deviceAction: new GenuineCheckDeviceAction({ input: {} }), - }); - let deviceId: string | undefined; - - return await new Promise((resolve, reject) => { - let settled = false; - let subscription: { unsubscribe: () => void } | undefined; - const settle = (fn: () => void) => { - if (settled) return; - settled = true; - clearTimeout(timer); - subscription?.unsubscribe(); - fn(); - }; - const timer = setTimeout(() => { - settle(() => { - action.cancel(); - reject(new Error('Ledger server Genuine Check timed out')); - }); - }, timeoutMs); - subscription = action.observable.subscribe({ - next: state => { - if (state.status === DeviceActionStatus.Pending) { - const intermediate = state.intermediateValue as - | { - deviceId?: Uint8Array; - requiredUserInteraction?: string; - } - | undefined; - if (intermediate?.deviceId instanceof Uint8Array && !deviceId) { - deviceId = Buffer.from(intermediate.deviceId).toString('hex'); - } - if (intermediate?.requiredUserInteraction) { - bridge.onInteraction?.(intermediate.requiredUserInteraction); - } - return; - } - if (state.status === DeviceActionStatus.Completed) { - const output = state.output as { isGenuine?: boolean }; - settle(() => { - if (output.isGenuine && !deviceId) { - reject( - new Error( - 'Ledger server Genuine Check succeeded without a physical-device DSID' - ) - ); - return; - } - resolve({ - isGenuine: output.isGenuine === true, - deviceId: output.isGenuine === true ? deviceId : undefined, - }); - }); - return; - } - if (state.status === DeviceActionStatus.Error) { - settle(() => reject(state.error)); - } - }, - error: error => settle(() => reject(error)), - }); - }); - } finally { - try { - await dmk.stopDiscovering(); - } catch { - // Discovery may have stopped after the first emitted device. - } - if (sessionId) { - try { - await dmk.disconnect({ sessionId }); - } catch { - // Best-effort cleanup; the result has already been decided. - } - } - dmk.close(); - } -}; diff --git a/packages/hwk-ledger-adapter/tsup.config.ts b/packages/hwk-ledger-adapter/tsup.config.ts index 7217d190e..9903ebca5 100644 --- a/packages/hwk-ledger-adapter/tsup.config.ts +++ b/packages/hwk-ledger-adapter/tsup.config.ts @@ -1,10 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: { - index: 'src/index.ts', - 'attestation-relay': 'src/attestationRelay/index.ts', - }, + entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, clean: true, @@ -21,6 +18,5 @@ export default defineConfig({ 'react-native', 'react-native-ble-plx', 'rxjs', - 'ws', ], }); diff --git a/scripts/monitor-config.json b/scripts/monitor-config.json index 6450c0504..c65064022 100644 --- a/scripts/monitor-config.json +++ b/scripts/monitor-config.json @@ -29,8 +29,7 @@ "hwk-trezor-connector-electron-ble" ], "specialProjects": { - "hd-web-sdk": ["dist", "build"], - "hwk-ledger-adapter": ["dist", "package.json"] + "hd-web-sdk": ["dist", "build"] }, "ignoredPaths": [ "node_modules", From ec25dc45c9beb0d72cfa8adea65df9332f094ab0 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Tue, 28 Jul 2026 16:20:28 +0800 Subject: [PATCH 09/12] fix: expose sandbox-safe Trezor BLE constants --- .../hwk-trezor-connector-electron-ble/package.json | 10 ++++++++++ .../hwk-trezor-connector-electron-ble/tsup.config.ts | 2 +- scripts/monitor-config.json | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/hwk-trezor-connector-electron-ble/package.json b/packages/hwk-trezor-connector-electron-ble/package.json index e4b234d67..45e9bb8a8 100644 --- a/packages/hwk-trezor-connector-electron-ble/package.json +++ b/packages/hwk-trezor-connector-electron-ble/package.json @@ -27,6 +27,16 @@ "types": "./dist/main.d.ts", "default": "./dist/main.js" } + }, + "./constants": { + "import": { + "types": "./dist/constants.d.mts", + "default": "./dist/constants.mjs" + }, + "require": { + "types": "./dist/constants.d.ts", + "default": "./dist/constants.js" + } } }, "files": [ diff --git a/packages/hwk-trezor-connector-electron-ble/tsup.config.ts b/packages/hwk-trezor-connector-electron-ble/tsup.config.ts index 57246aa60..eee9476c9 100644 --- a/packages/hwk-trezor-connector-electron-ble/tsup.config.ts +++ b/packages/hwk-trezor-connector-electron-ble/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/index.ts', 'src/main.ts'], + entry: ['src/index.ts', 'src/main.ts', 'src/constants.ts'], format: ['cjs', 'esm'], dts: true, clean: true, diff --git a/scripts/monitor-config.json b/scripts/monitor-config.json index c65064022..d4939ef7e 100644 --- a/scripts/monitor-config.json +++ b/scripts/monitor-config.json @@ -29,7 +29,8 @@ "hwk-trezor-connector-electron-ble" ], "specialProjects": { - "hd-web-sdk": ["dist", "build"] + "hd-web-sdk": ["dist", "build"], + "hwk-trezor-connector-electron-ble": ["dist", "package.json"] }, "ignoredPaths": [ "node_modules", From 241c0c2ccde3bc070d7a85d0433b4cbb273bf15c Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Tue, 28 Jul 2026 20:26:46 +0800 Subject: [PATCH 10/12] docs: align attestation handoff with client flow --- HANDOFF-device-attestation.md | 69 ++++++++-------------- docs/device-attestation-voucher-backend.md | 43 +++++--------- 2 files changed, 38 insertions(+), 74 deletions(-) diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index 05243b5e2..96029a53d 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -1,13 +1,13 @@ # 交接文档:第三方硬件设备唯一 ID + 账户名迁移 -> 最后更新:2026-07-27 +> 最后更新:2026-07-28 > 本文保留了早期调查过程;以本节状态和 > `docs/device-attestation-voucher-backend.md` 的生产边界为准。 > **重要纠正**:早期 T3W1 真机结果只覆盖 Optiga + Tropic,不能代表当前要求的 -> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。Ledger 已改成 -> “本地 OneKey 服务端拥有 DMK 状态机、App 只转发物理设备 APDU”;它不是离线证明, -> 本地实现只用于 Monorepo 联调,生产后端按本文协议独立实现。 +> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。客户端已移除 +> 本地 Node/WSS/DMK 服务端,只保留最小真机集成检查;生产 Ledger relay 和发券信任 +> 边界由后端按 `docs/device-attestation-voucher-backend.md` 独立实现。 --- @@ -16,13 +16,13 @@ | 项 | 状态 | 真机验证 | | ----------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | | Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已补齐流式 proof + Optiga/Tropic/MCU 三层验证 | ⏳ 当前版本待 Safe 7 重新实测;官方 ML-DSA 真向量已通过 | -| Ledger 设备唯一 ID(服务端 DMK Genuine Check) | ✅ 本地 TS 服务端 + 一次性 WSS APDU relay 已实现 | ⏳ 新状态机待连接 Nano X 真机跑通 | +| Ledger 设备唯一 ID(官方 DMK Genuine Check) | ✅ SDK 直接厂商验真;客户端无本地 Node relay | ✅ Nano X 真机已返回 genuine + DSID | | Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | | Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | | app-monorepo 调试页(Developer→Gallery→DeviceAuthenticity) | ✅ 已加 | ✅ 已用它测通两端 | | 账户名读取 · Ledger | ✅ Desktop 已实现只读清单 | ✅ 本机读出 Ethereum 名称/地址 | | 账户名读取 · Trezor | ✅ Desktop 已实现 Suite 缓存只读清单 | ✅ 本机读出 Bitcoin 首个 receive address;不连接设备 | -| 本地 mock 发券 | ✅ 本地可信服务拥有 challenge→ 验真 → 发券全流程 | Trezor 当前版待真机;Ledger WSS/DMK 集成测试已通过,待真机 | +| 本地 mock 发券 | ✅ 仅作 UI 联调:SDK 真机验真后生成 DEV 券 | 不作为后端可信凭证;生产发券由后端独立实现 | --- @@ -38,8 +38,8 @@ | 仓库 | 分支 | 作用 | | -------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| `~/Development/OnekeyWork/hardware-js-sdk` | `codex/device-attestation-voucher-proof` | SDK、Ledger 本地/远端 relay server、设备 APDU bridge | -| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `codex/third-party-device-onboarding-rewards` | 设备详情页、Desktop 本地服务启动、Claim 与账户名清单 | +| `~/Development/OnekeyWork/hardware-js-sdk` | `codex/device-attestation-voucher-proof` | Trezor/Ledger 真机验真 SDK 与设备传输能力 | +| `~/Development/OnekeyWork/app-monorepo-fix-ledger` | `codex/third-party-device-onboarding-rewards` | 设备详情页、最小本地验真 UI、Claim 演示与账户名清单 | | 参考(只读) | — | `~/Development/OnekeyWork/trezor-suite`、`trezor-firmware`、`node_modules/@ledgerhq/device-management-kit`(DMK) | > ⚠️ 关键前提:设备认证只能加在**新 `hwk-*` 栈**(`hwk-trezor-adapter`/`hwk-ledger-adapter`/`hwk-trezor-connector`),**不能**加在 legacy `packages/core`(`DeviceVerify.ts` 那套)——legacy 的 wire protobuf 里没有 `AuthenticateDevice` 消息、features 也没有 `internal_model`。 @@ -108,40 +108,20 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 **真机结果**(Nano X):`Verified ✅`,`deviceId=818706ff78239d1ab642df69c1d1fdd48acfc59ba282f4139383cfee31adfcdc`(走 genuine-check backend)。`attestationPubKey` 字段的真机验证**待做**(清缓存重启 Desktop 后看 Ledger 结果多一行 `attestationPubKey: 04...`)。 -#### 3.2.1 当前 Monorepo 本地“服务端拥有 DMK”实现 - -现在 Claim 不再调用客户端 `verifyDeviceAuthenticity()` 后相信它返回的布尔值。真实路径是: - -1. Desktop background 启动 `LedgerLocalAttestationServer`,只监听 - `127.0.0.1`,创建 32-byte 一次性 token 和 5 分钟 ticket; -2. SDK 的 `LedgerAdapter.runDeviceAttestationApduBridge()` 独占当前已连接 - Ledger session,并暂停 DMK session refresher; -3. App 连接 ticket 的 `ws://127.0.0.1/.../`,只处理 - `apdu-request → exchangeDeviceAttestationApdu → apdu-response`; -4. 服务端自己的 DMK 使用自定义 `TransportFactory`,执行官方 - `GenuineCheckDeviceAction`,并直接连接 Ledger HSM; -5. 只有服务端内部 `ticket.result` 的 `isGenuine + deviceId` 能进入发券函数。 - WSS 发给客户端的 `result` 仅用于 UX,客户端不能提交它来发券; -6. success、failure、断线或取消都会恢复 refresher 并释放设备队列。 - -代码都在 App Monorepo: - -- `packages/kit-bg/src/services/ServiceThirdPartyHardware/ledgerLocalAttestationServer/` - - `LedgerLocalAttestationServer.ts`:一次性 ticket/WSS 会话; - - `relayTransport.ts`:远端 APDU → DMK `TransportFactory`; - - `runLedgerDmkGenuineCheck.ts`:服务端官方 DMK action; - - `protocol.ts`:严格消息解析、大小/次数/顺序限制。 -- SDK 只保留 `LedgerConnectorBase.ts` 的 - `start/exchange/stopDeviceAttestationApduBridge`; -- SDK `LedgerAdapter.ts`:`runDeviceAttestationApduBridge()` 在整个 relay 生命周期独占 - DeviceJobQueue; -- App Desktop: - `ServiceThirdPartyHardware/ledgerLocalAttestationBridge.desktop.ts`。 - -这套 Node 服务不从 SDK 发布,也不作为生产后端包。后端团队按 -`docs/device-attestation-voucher-backend.md` 的状态机和 WSS contract 独立实现; -App 的 APDU client 后续只需把本地 `ws://127.0.0.1` ticket 换成后端返回的 -`wss://attestation.`。 +#### 3.2.1 当前 Monorepo 最小验证实现 + +App Monorepo 不再内置 Node/WSS/DMK 服务端。设备详情页的开发按钮只做两件事: + +1. 调用 `adapter.hw.verifyDeviceAuthenticity()` 跑真实设备/厂商验真; +2. 验真成功后生成一张本地 `DEV-LOCAL-*` 券,验证 UI 流程。 + +Trezor 会使用 background 生成的 32-byte challenge;Ledger 直接运行 SDK 中的官方 +DMK Genuine Check 并连接 Ledger HSM。两者结果都是真实集成结果,但客户端返回的 +`verified/deviceId` **不能作为生产后端发券凭证**。 + +后端团队按 `docs/device-attestation-voucher-backend.md` 独立实现 challenge、Ledger +relay、状态机、鉴权、幂等和发券事务。客户端当前没有可复用的本地服务端代码,也不 +直接依赖 `ws`、`rxjs`、`purify-ts` 或 Ledger DMK 包。 ### 3.3 Review 修复状态(Fable 5 审查) @@ -182,9 +162,8 @@ yarn workspace @onekeyhq/desktop dev **注意**:缓存不能边跑边清(webpack 会立刻重新生成)——必须先停 Desktop 再清。 **构建环境**:`tsup` 曾部分缺失,`yarn install` 可恢复(本会话已跑过一次)。 -> 当前使用 `yarn debug:watch` 同步本地 SDK,不发 alpha 包。 -> `hwk-ledger-adapter` 只需同步 `dist`;本地 relay 已归属 App Monorepo,不需要同步 -> SDK `package.json`。 +> 当前使用 `yarn debug:watcher` 同步本地 SDK,不发 alpha 包。 +> `hwk-ledger-adapter` 只需同步 `dist`;App Monorepo 不再包含本地 relay。 --- diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md index d04b76a72..29fa05915 100644 --- a/docs/device-attestation-voucher-backend.md +++ b/docs/device-attestation-voucher-backend.md @@ -279,18 +279,12 @@ JWS/COSE receipt。单纯透明代理 Ledger WSS、由客户端计算 `isGenuine #### 4.3.1 后端需要独立实现的 DMK 状态机 -App Monorepo 中有一份只用于本地联调的参考实现: - -`packages/kit-bg/src/services/ServiceThirdPartyHardware/ledgerLocalAttestationServer/` - -它不属于 Hardware SDK、不通过 npm subpath 发布,也不是生产后端交付物。后端团队 +App Monorepo 不再内置 Node/WSS/DMK 参考服务。客户端只保留真机集成检查;后端团队 需要使用自己的服务框架、鉴权、存储和部署方式独立实现下面的状态机与 WSS contract。 -本地 `LedgerLocalAttestationServer` 运行官方 -`GenuineCheckDeviceAction`,不是模拟 `isGenuine`。它通过自定义 DMK -`TransportFactory` 把每条 APDU 交给 WSS 客户端,客户端再用已连接的 USB/BLE -session 发给物理 Ledger。服务端同时直连 Ledger 官方 HSM endpoint,因此验证时必须 -联网。 +后端运行官方 `GenuineCheckDeviceAction`,通过自定义 DMK `TransportFactory` 把每条 +APDU 交给 WSS 客户端,客户端再用已连接的 USB/BLE session 发给物理 Ledger。后端 +同时直连 Ledger 官方 HSM endpoint,因此验证时必须联网。 推荐持久化状态机: @@ -316,7 +310,7 @@ App -> Server: apdu-error(requestId, message) Server -> App: interaction / result / error ``` -实现已经约束: +后端实现必须约束: - 32-byte CSPRNG token、单次消费、默认 TTL 5 分钟; - 严格版本/字段/hex/model 校验; @@ -325,27 +319,18 @@ Server -> App: interaction / result / error - `isGenuine=false` 时丢弃任何 `deviceId`; - 只有服务端保存的 Promise/数据库 session 能完成 claim,客户端 `result` 不具备授权力。 -后端实现时可参考本地 `protocol.ts`、`relayTransport.ts` 和 -`runLedgerDmkGenuineCheck.ts` 的行为,但不要求复用其代码。生产必须将内存 ticket -store 替换为 Redis/数据库,把本地 `ws://` 升级为 TLS `wss://`,接入 OneKey 登录态、 -限流、审计和多实例 sticky routing。 +生产必须使用 Redis/数据库保存 ticket,把连接暴露为 TLS `wss://`,接入 OneKey +登录态、限流、审计和多实例 sticky routing。 同一 attestation session 的 WSS 与 DMK runner 必须由同一 worker 拥有,或用严格有序的 消息队列转发。 -### 4.4 当前本地服务的真实性和边界 - -本地服务只替换部署位置,不替换密码学验证: +### 4.4 当前客户端最小集成检查的边界 -- Trezor:本地可信 background 生成 32-byte challenge,设备签名,background - 使用固定生产根证书重新验证原始 Optiga/Tropic/MCU 证明,验证成功后才生成开发券。 - 这段 verifier 和 evidence DTO 可以原样移动到后端。 -- Ledger:本地可信 Node 服务运行官方 DMK Genuine Check 并连接 Ledger HSM;App - background 只转发 APDU。服务端内部 ticket 拿到真实 verdict + DSID 后才生成开发券。 - 这已经是上一节的“服务端 DMK + 远程 APDU transport”,只是 WSS 当前监听 - `127.0.0.1`;迁到远端后不改变信任边界。 -- `runTrustedLocalMockDeviceClaim` 接收的是执行真实验真的闭包,不接受 renderer - 提交的 attestation DTO;challenge 在执行器内部生成,领券前不会交给不可信调用方 - 决定验证结果。 +- Trezor:background 生成 32-byte challenge,SDK 让设备签名并验证生产证书链。 +- Ledger:SDK 直接运行官方 DMK Genuine Check 并连接 Ledger HSM。 +- 两条路径成功后只生成本地开发券,用于验证 UI 和设备通讯。 +- 客户端结果不是后端可信凭证。生产后端不能相信客户端提交的 + `{ verified, deviceId }`,必须按前文自己验证或见证完整会话。 ### 4.5 平台与联网矩阵 @@ -353,7 +338,7 @@ store 替换为 Redis/数据库,把本地 `ws://` 升级为 TLS `wss://`,接 | ----------------------------------- | ------------------------------- | ----------------- | ------------------------------- | --------------------------------- | | Trezor 本地证书验证 | 支持,USB/BLE 取 proof | WebUSB 可用时支持 | 有对应 USB/BLE transport 时支持 | 不需要;拿到 proof 后可完全离线验 | | Trezor 后端发券 | 支持 | 支持 | 支持 | 需要连接 OneKey 后端提交 proof | -| Ledger 当前本地 DMK server | 支持 WebHID/BLE | Node relay 不内置 | 当前本地 server 仅 Desktop | 必须连接 Ledger HSM | +| Ledger 客户端集成检查 | 支持 WebHID/BLE | 支持对应 transport| 支持对应 transport | 必须连接 Ledger HSM | | Ledger 生产服务端 DMK + APDU bridge | 支持 | 支持 | 支持 | 必须连接 OneKey 后端和 Ledger HSM | 操作系统不是主要限制,真正的限制是该端是否具备对应 USB/BLE transport。Trezor From b16f2ac23eaa281286b031175934ec7ea77e78d2 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Tue, 28 Jul 2026 20:54:44 +0800 Subject: [PATCH 11/12] fix: align Trezor attestation with vendor policy --- HANDOFF-device-attestation.md | 48 ++++++------- docs/device-attestation-voucher-backend.md | 7 +- packages/hwk-trezor-adapter/jest.config.js | 2 +- packages/hwk-trezor-adapter/package.json | 1 - .../__tests__/mcuFixture.ts | 8 --- .../__tests__/verifyAuthenticityProof.test.ts | 72 +------------------ .../src/deviceAuthenticity/config.ts | 2 - .../src/deviceAuthenticity/index.ts | 47 +++--------- .../deviceAuthenticity/trezorRootKeysMLDSA.ts | 6 -- .../src/deviceAuthenticity/types.ts | 1 - .../verifyAuthenticityProof.ts | 53 ++------------ .../deviceAuthenticity/verifySignatures.ts | 10 --- packages/hwk-trezor-adapter/tsup.config.ts | 4 -- yarn.lock | 26 ------- 14 files changed, 43 insertions(+), 244 deletions(-) delete mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts delete mode 100644 packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts diff --git a/HANDOFF-device-attestation.md b/HANDOFF-device-attestation.md index 96029a53d..04b5cf824 100644 --- a/HANDOFF-device-attestation.md +++ b/HANDOFF-device-attestation.md @@ -4,10 +4,10 @@ > 本文保留了早期调查过程;以本节状态和 > `docs/device-attestation-voucher-backend.md` 的生产边界为准。 -> **重要纠正**:早期 T3W1 真机结果只覆盖 Optiga + Tropic,不能代表当前要求的 -> Optiga + Tropic + MCU/ML-DSA 三层完整证明已经在当前 App 会话跑通。客户端已移除 -> 本地 Node/WSS/DMK 服务端,只保留最小真机集成检查;生产 Ledger relay 和发券信任 -> 边界由后端按 `docs/device-attestation-voucher-backend.md` 独立实现。 +> **重要纠正**:客户端验真已收敛为 Trezor Connect 的生产策略:所有支持机型验证 +> Optiga,T3W1 / Safe 7 再验证 Tropic。设备返回的 MCU 字段仍保留在 raw proof 中, +> 但不使用自定义 ML-DSA/serial 规则覆盖厂商结果。客户端已移除本地 Node/WSS/DMK +> 服务端,只保留最小真机集成检查;生产 Ledger relay 和发券信任边界由后端独立实现。 --- @@ -15,7 +15,7 @@ | 项 | 状态 | 真机验证 | | ----------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | -| Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 已补齐流式 proof + Optiga/Tropic/MCU 三层验证 | ⏳ 当前版本待 Safe 7 重新实测;官方 ML-DSA 真向量已通过 | +| Trezor 设备唯一 ID(AuthenticateDevice 本地验签) | ✅ 流式 proof + 官方 Optiga/Tropic 验证策略 | ⏳ 当前修复待 Safe 7 重新点击验证 | | Ledger 设备唯一 ID(官方 DMK Genuine Check) | ✅ SDK 直接厂商验真;客户端无本地 Node relay | ✅ Nano X 真机已返回 genuine + DSID | | Ledger 裸 attestation 公钥提取(transport tap) | ✅ 已实现,已同步 | ⏳ 待测 `attestationPubKey` 字段 | | Review 修复(H1/H2/M1/M2/M3) | ✅ 已应用 + 单测 | H1 已随 T3W1 真机验证 | @@ -53,8 +53,8 @@ **机制**:`AuthenticateDevice({ challenge, stream: true })` → 旧固件直接返回 `AuthenticityProof`,Safe 7 当前固件先返回各 proof part 的大小,再由主机以 `GetAuthenticityProofChunk` 分块取回。主机侧使用 Trezor 公开的生产根验证证书与 -challenge 签名。T3W1 强制同时验证 Optiga(P-256)、Tropic(Ed25519) 和 -MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 +challenge 签名。T3W1 按 Trezor Connect 的生产策略同时验证 +Optiga(P-256) 和 Tropic(Ed25519)。MCU proof 会随 raw evidence 返回,但不参与当前客户端 verdict。 **支持机型**:Safe 3(T2B1/T3B1)、Safe 5(T3T1)、T3W1(+Tropic)。**Trezor One(T1B1)/Model T(T2T1) 无安全芯片,不支持**。 @@ -69,11 +69,11 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 **新增验签模块**:`packages/hwk-trezor-adapter/src/deviceAuthenticity/`(从 `@trezor/device-authenticity` 移植): - `x509certificate.ts`(DER/X.509 解析,自包含,原样移植) -- `verifySignatures.ts`(P-256 + Ed25519 + ML-DSA-44;P-256 **关 lowS**——硬件签名非 low-S) +- `verifySignatures.ts`(P-256 + Ed25519;P-256 **关 lowS**——硬件签名非 low-S) - `verifyAuthenticityProof.ts`(根 CA→CA→ 设备 →challenge 全链 + CA 黑名单钩子) -- `config.ts` + `trezorRootKeysMLDSA.ts`(Trezor 公开生产根) -- `index.ts`(`authenticateDeviceFromProof()`:整体 try/catch 兜底、T3W1 强制三层 + serial 交叉校验、debug-key 标记) -- `__tests__/verifyAuthenticityProof.test.ts`(包含 Trezor Suite 官方 MCU/ML-DSA 真证书和签名黄金向量) +- `config.ts`(Trezor 公开 Optiga/Tropic 生产根) +- `index.ts`(`authenticateDeviceFromProof()`:整体 try/catch 兜底、T3W1 强制 Optiga + Tropic、debug-key 标记) +- `__tests__/verifyAuthenticityProof.test.ts`(Optiga/Tropic 真证书和签名黄金向量) **设备调用接线**(3 处): @@ -81,7 +81,7 @@ MCU(ML-DSA-44),并要求三层设备证书 serial 一致。 - `hwk-trezor-connector/src/index.ts`:switch 加 `case 'authenticateDevice'` 发 `AuthenticateDevice` 消息 - `hwk-trezor-adapter/src/adapter/TrezorAdapter.ts`:`verifyDeviceAuthenticity` 方法 -**真机结果**(T3W1):`Verified ✅`,`deviceId=serialNumber` = `3437333132303432353030383646`(ASCII "4731204250086F"),`rootPubKey` 命中 T3W1 PROD Optiga 根键(非 debug)。**证明 H1 修复(optiga+tropic+serial 三重)在真机通过**。 +**历史真机结果**(T3W1):`Verified ✅`,`deviceId=serialNumber` = `3437333132303432353030383646`(ASCII "4731204250086F"),`rootPubKey` 命中 T3W1 PROD Optiga 根键(非 debug)。2026-07-28 收敛官方策略后的版本需要重新点击一次 Claim 验证。 ### 3.2 Ledger(必须走 Ledger 后端) @@ -125,15 +125,15 @@ relay、状态机、鉴权、幂等和发券事务。客户端当前没有可复 ### 3.3 Review 修复状态(Fable 5 审查) -- **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 代码修复:T3W1 强制 - Optiga + Tropic + MCU/ML-DSA,并交叉校验 serial;当前三层版本待 Safe 7 真机重测。 +- **H1**(T3W1 只验 Optiga = 芯片移植攻击面)→ ✅ 按 Trezor Connect 生产策略, + T3W1 强制 Optiga + Tropic;不再追加未经厂商 API 定义的 MCU/serial verdict。 - **H2**(Ledger 假 id 易误用)→ ✅ 已被 genuine-check 重构取代:现在 Ledger 有真 `verified` + 后端验证的 deviceId。 - **H3**(Ledger `E0 52` p1/p2 是猜测 + 批次证书风险)→ ✅ 真机坐实"裸 E0 52 不成立",已改走 genuine-check(彻底绕开该风险)。 - **M1**(无吊销通道)→ ✅ 加 CA 公钥黑名单钩子(默认空)。 - **M2**(畸形证书抛未捕获异常)→ ✅ `authenticateDeviceFromProof` 整体 try/catch → `verified:false`。 - **M3**(debug key 无守卫)→ ✅ 参数改名 `dangerouslyAllowDebugKeys` + 结果带 `usedDebugKey` 标志。 -- 已补:ML-DSA-44、Safe 7 流式 proof protobuf/连接逻辑和生产根;仍待当前 App - 真机完整跑通。L1(CA notBefore 未来检查,Low)仍未做。 +- 已补:Safe 7 流式 proof protobuf/连接逻辑和 Optiga/Tropic 生产根;仍待当前 App + 重新点击真机验真。L1(CA notBefore 未来检查,Low)仍未做。 --- @@ -142,20 +142,20 @@ relay、状态机、鉴权、幂等和发券事务。客户端当前没有可复 app-monorepo 通过**文件拷贝**吃 SDK(node_modules 里是 dist 副本,不是软链)。每次改 SDK 都要走完整链路,**光重启不清缓存不生效**: ```bash -# 1. 在 hardware-js-sdk 构建改过的包 +# Terminal A:同步所有本地 SDK dist 到 app-monorepo cd ~/Development/OnekeyWork/hardware-js-sdk -yarn workspace @onekeyfe/hwk-ledger-adapter build # 或改了哪个包就构建哪个 +yarn debug:watcher -# 2. 同步 dist 到 app-monorepo(cp -R 只覆盖不删) -cp -R packages/hwk-ledger-adapter/dist/. \ - ~/Development/OnekeyWork/app-monorepo-fix-ledger/node_modules/@onekeyfe/hwk-ledger-adapter/dist/ +# Terminal B:构建并监听改过的包 +yarn workspace @onekeyfe/hwk-trezor-adapter dev +# Ledger 改动时换成 @onekeyfe/hwk-ledger-adapter -# 3. 【关键】停掉 Desktop dev,清 webpack 持久缓存(webpack 把 node_modules 当不可变,不清就用旧代码!) +# 如果 Desktop 已缓存旧的 node_modules bundle:停掉 Desktop 后清一次缓存 cd ~/Development/OnekeyWork/app-monorepo-fix-ledger rm -rf apps/desktop/node_modules/.cache node_modules/.cache/web node_modules/.cache/babel-loader -# 4. 重启 Desktop(第一次无缓存会慢几分钟) -yarn workspace @onekeyhq/desktop dev +# 再启动 Desktop +corepack yarn app:desktop ``` **判断新代码是否生效**:看报错信息变没变(每版报错串不同)。曾因缓存导致改了半天还跑旧代码、报旧的 `GET CERTIFICATE failed with status 6604`。 diff --git a/docs/device-attestation-voucher-backend.md b/docs/device-attestation-voucher-backend.md index 29fa05915..9f4288114 100644 --- a/docs/device-attestation-voucher-backend.md +++ b/docs/device-attestation-voucher-backend.md @@ -247,8 +247,9 @@ Ledger 客户端提交的 `verified`、`deviceId`、`attestationPubKey` 只能 3. 使用后端内置、版本化的生产 root 集合及吊销表; 4. 验证证书链角色、有效期、签名算法与 nonce 签名; 5. 从证书推导型号,客户端的 `deviceModelHint` 只做一致性检查; -6. T3W1 强制 Optiga + Tropic + MCU/ML-DSA 三层证明,三张设备证书的 - subject serial 必须都存在且一致;缺任一层直接拒绝; +6. T3W1 至少按当前 Trezor Connect 生产策略强制验证 Optiga + Tropic; + `mcu_*` 字段可随 raw proof 保存,但在后端采用 MCU/ML-DSA 前必须先对齐 + Trezor 官方库的版本化策略和测试向量,不能使用客户端自创条件; 7. 不接受 debug/staging root; 8. 从已验证证书推导权威 vendor DSID: - 有 subject serial:`trezor:v1:`; @@ -492,7 +493,7 @@ AND postAddTask(walletId, campaignId) has never completed ## 9. 上线门槛 - Trezor:challenge 置换、重放、错用户、过期、debug root、错链、缺 - Optiga/Tropic/MCU 任一层、serial 不同、畸形/超大 DER 全部拒绝; + Optiga(T3W1 还缺 Tropic)、畸形/超大 DER 全部拒绝; - Ledger:伪造 `{verified:true, deviceId}` 必须拒绝;relay session 过期、错用户、重放、APDU 篡改、non-genuine、缺 certificate、并发双提交全部拒绝; - 两端:地址签名与 challenge 不匹配、活动重复领取、事务中途失败不发券; - 名称同步:只在首次新建钱包触发、只改用户确认项、加密/云端来源不误报成功; diff --git a/packages/hwk-trezor-adapter/jest.config.js b/packages/hwk-trezor-adapter/jest.config.js index 64695eac8..c02d76b0d 100644 --- a/packages/hwk-trezor-adapter/jest.config.js +++ b/packages/hwk-trezor-adapter/jest.config.js @@ -1,7 +1,7 @@ module.exports = { preset: '../../jest.config.js', testEnvironment: 'node', - transformIgnorePatterns: ['/node_modules/(?!@noble/(post-quantum|curves|hashes|ciphers)/)'], + transformIgnorePatterns: ['/node_modules/(?!@noble/(curves|hashes)/)'], modulePathIgnorePatterns: ['node_modules', '/dist'], moduleNameMapper: { '^@onekeyfe/hwk-adapter-core$': '/../hwk-adapter-core/src/index.ts', diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index 6c4479e5d..14381c373 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -45,7 +45,6 @@ "dependencies": { "@noble/curves": "^1.9.7", "@noble/hashes": "^1.3.3", - "@noble/post-quantum": "^0.6.1", "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.1", "tslib": "^2.6.2" diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts deleted file mode 100644 index 044a32bb8..000000000 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/mcuFixture.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Golden ML-DSA vector synchronized from Trezor Suite device-authenticity mocks. -export const CHALLENGE = '29d0be0f3cb191c80d108359c64d22984a77ad8b99433814be31db0b6e9e7920'; -export const DEVICE_CERT_MCU = - '30820fdf30820655a003020102020868b1982d44540915300b06096086480165030403113054310b300906035504061302435a311e301c060355040a0c155472657a6f7220436f6d70616e7920732e722e6f2e3125302306035504030c1c5472657a6f72204d616e75666163747572696e6720526f6f742043413020170d3235303832393132353934375a180f32303535303832323132353934375a3046311b301906035504030c1254335731205472657a6f7220536166652037311730150603550405130e3437333032313932363030303972310e300c060355042e130553544d333230820532300b06096086480165030403110382052100f71c837210b55caed1d1a401f232931ee892808bb4bbd186175e54b0fffe27a15494e5b4f548194e7c099689df4c8b207bbaa6e8e26e457581988d09fb85f5896ac13cc2e5832963099ab5dc844cdd5d3515abe7c9a2253aec5e6e98be9f5e1b8000b5c5cc52a3985ef8e0b94525811d8e31f8ea8f20d359d0b81e174a7da937a129606583f1321bcdb5067c45af60e13dd5d32ec8a328ed724882dff570c11a7bda15d8cd5995c25261887cb64c830922618df5be0d5bbd5b8a93af1baf677299a6abb986f1e88792fba0a22b223729e1a05cc55a53eb4d88ffe111aaa7cddc97764ac9438133e52528ebe622aab63ef921fa689cfbc5841fdfd8c6c8aaaf52cf55f0d6ce13e26e593bec6b57a1ef9133a6d98fbfaf630abb91a58ec27db450f31339d4ec1d3e9c1870ac8b7abaff117b41fa5adebd226e1f5326d1c06920664d71b633bef50542eee4e518ba478abafc4d539b1c9799183bc83b95bf5e0ad1df9dbd7160c053aad0ce825e5615083c0cca262a552d0b1ecb3e08d46b5364403beb1182155c1421f79bd6c53a55869bc81928b93e4902b9546fa76e47d7cb7ccb11321788e10cd354b2c0a9f4e85cfd0cbcd3827bda7e2ce9930906af9cb0ea43612847183fbcf0fea73109fbf164ae3257e02119d53614e0e4257c975c6fb9e247293b1abad833c8e16bded2f151d2dfd5e98cd15fcb930836b66a74044223715bc7e540785bd45bbbae59c7a9520ce03747f4dcc5faeeec5f99c21f5b6aa0b985e7f0690e4c58c15b32003d8a2d4c0075e1689b814e0855ab418e2853ef28f8e7e4619ad425c720ff7315d2a311761bab30c8ac9708a539b35349559b946310a7889fb479e451f396125d48fb246346aff8577e26d1ea58841ca35a6dbac02cf568d5395c399aeb44b18c6e87ceb7811bf34619e90585bbcf64521a4fd2359c5fd923b9bffc3fd423d8ce0712bcc7d7747f6d6542125449afa75adc154a758578f1cfcd8751fdf2a18c516eed70fe0b196d7917b2934dce7a3e67d204bbb4d1430824aa5e803675319715f58367f2ac7e829bc314f1f87ccd78f5e226d66d2d8f031c60fb592323d10282ad24673f6498d643f0d5fb348e00ac94d3352a6c015df8c38af4d350e4913020171994637f4486ad8ac9928582142c983c4653da7c4a2275a3083ce9cc18fef70890eb6ee91067a18722aacbbb50b43050f7fdf1721ade8c0908d05686c7db0a7c2e63c997afbb85c34eb8a0666e382c635b49c301b12887fe96a9d266ff59be7d75ef43e2a583fc012034fa32e06241bd1d24a3f8a9ddeecacba68fd679b72c50c46d9ce1f757e83f94cfbc49069bc4137bf2384ad5c2c5cdea947e64c47cd5382a0ec9e05f102152b4d8ee4fae971df142ee29ee0b094866ca7e5c4232f63e1683d6406ad43178d9ae0360c8ef43d690ab6776f04efbf9704b4e51c228ba4dfa7d58db1b9d5a1526559c4c6c0531e9fc3531718142df0de39539f1f45bc60df753e8ccddab5c2874b4f1124869c21a7701f92fcec0b3d56d79713e934f6617fb2295b29a2bc3f90e20346547ecbe1118a8f86580e310ea6b40e7445c12e1ad7c2ad11cdf090e7d00fd49c56a8b23f5bd3d01a2a8f4b30d619d57dc2158b3a2ef0ba51878e414c7f1da5711aba389fa9944ae6f1012f24a8e9b0340cc1373339a05c438983660a1525fcd93a60011df2d3749e2e19f0096bc0af765c5bf350bb414ce6797888f470624444f1d69eb8d8d4f8f556e3d3e1f27a3325633e3560c489f9aad22c50c09b7f759bbcf44eaef83389adead48884e2a9d865a54fb21d75f2c2a6fb67a7231d69a75e1ab7e16f11fe91e26a341303f300e0603551d0f0101ff040403020080300c0603551d130101ff04023000301f0603551d230418301680140bc62f01e47558497cea38acf5d4e8a8b673306a300b06096086480165030403110382097500911e8268e98e6ca111577c697ac0938864b0480c0045e01838033624ad2956c40917ec55b992d7c0ebc2945b37a4338cd7bea024679281d14f03760ab7fd66fdf3b5160f4e3164e6a19fdd414e72ee2ff01d6a4ebd51dda6321bc12f3e6d3f3d6082b1c94db2a73aab60857ea8cc97f825abcc8fc98dbb053631bdc7fd028e5bc4639ac5277b1415491b4f77830af6916f312646f25973dba75e8d77c06d9c71c5a4bf31c3c5697213fb396512e77316ab0ba7018eda7f140a1ae5c5826e263a581e1eda33a6ab55a57252f7088c12a47f5396df72f1f6911d9ac76cb7cb7e0a0f0015b0f49032015a14293a2a383e475af8348578dcf82868afdb85942bf5d7d8c349aa36691f95ab1b7fed97b9fc14a7bbc2c2d802b66e12236f221f3d39b57f9cc73285663ee2302fad3b012372d64ef83f9bec4ff1e8269d27f024f56ac1bd86dd11bb76fff9c9fae9944fbefe20fa8ad307855bf8d5f8e07d389de3bbdf0693f57df7616ca6f5b2037b20f03f09706b20c87fc8d2cc344f42ce1cef6d757640f8d1c45e7e26a66af4ca29f3f37003e2926cf6216acabb01b58d367f3e7f1bd409c0bc21d2e4875bc67608039db6fb314f990cb5115301994fec65f92cea6090adf930d7754b81451db6d85bb98b9f09e72d435d58d05a3b941c7069acc55ff8a4670696c791367b2de84c34435d17d65125e5b2d31704f5934863ad5a43deef62cb3cc8314b24ab1d33328bf036a735d08022198960f3a7bb7da1c740cdd082addb78423162e00dc3d46bec006d0d9bd972d1f2822351e8c3b4ba0a4b34efceb2aa07e27d9ee19e861ab87cc4a4ab0ccf1842053e0fc11d9e792fcf642f7c22f6cfa470c83c7eea66db066bb99bb0e313c6419a7d3a6933d650d2075b553cd99d4d1a773eb3c6611b013a7444a01d89bb59b76c92523efbab92399fd1c1820b0a46e413f5fdb7553776252543cea685e31adab2a2499149e6e8bf7779924d81c2714aab9e2d10995a07220fc8b69bd8188819356329a74fc782baeec84f9e5cfa7325fb47008f57d04b10d652bc1b5e317dfa71a4c379b07e636bdcb7a944920ddb4523a50d1f0b8e8735cc3ceb33c6c8c42888040e8e909dc8593edad2d6eed931a9881e662c72f43f837ad0274f8845268d544ac59a8d797075c761fcf4bbc892d4fbe031c70097f82157abae5eca1b0559828e422564a3636e6621cf40d0a59daf6738cca3c5ef1e1c51d9f2711335b6946a709c0fb131f4dbd8f89dd2e72d4a4eda83f6fb84f86569a9142a60f26757833aec49f015f18e73f142ea790af766bddbff3a3337ba6fef219834a597a6651bdac2e741e75ace4da1a934fc28e183a497682fc80ecc0e41a43edadcfab9d428ff10088abbec78889e855bad736bc0480d57231d5672ebc55ded6b4aba8a2fda397351f5ffe7d448b1ba55439d6fcc51110b76ce34e27fdaca5bbcd28bbda386ba47853a9e978a93aa3578babf398e67f30e47f18abdd9e4dfc89d66a8017a26417ae83440098ba749ffde54aba8ccd359202b98f6ca041f19b1df6b239d572202a4ca6fe98980bf49c8d3e39b7ce98f0730113720bf52b71005fea87105dd0364b004f734b669e6dad130327385c706c5004eddcbbda177b272319b37e32d82e873b6b5ec56972fa86c1f3555d3f62d8ef5cbc45ef64c31a21c819f16d1a0c985e080541f5fdb610efb5e2ff8dc17206b827feb646093b2aac95f1bad08ee3a0fbae40749c34da4188909923adf6b273437a102167c4a22d7b11a7c5df6876590261564825cdfde09cd87a1086cab90c786d650bced7e5f060de4e78007d7de9015a5887aba4b51be34b945f2f75d36562b9e8042155a8138f6a93d561fa4fb33b8cac57fa6627ba93ddf540ddf95c5430b1d539113f844c22f5378a1c083c093143918ffc0758f2ebdb7ecac11858571d9a3a03c833a7a834bc556409b80e295a83f825c728b72f59a12e68d840271544ac7bd48c40f8291ae2d96458eb7e59f720a8e073be1558d0b942438dab7f9007a6f1ed4ce55ce07ed28055383143ac7a61831c10d01c04530373f2b4cfe478f0bafc7d49dba637986c9af966a957eef5c15e203d192bca08521d7d4a234e0ea4ba1605ea7a98c4756fc06813ad71c5932583f61dae91f512a74ab1b4eeebb88fbbe9256d041fe8ea7ff15d757d9e07e6d0cbd38bd0cfe851bc60a616e6e7feec9d14e263e69b6e499096dd28fe1655f6032fc39678ffcd8002b1146dc4af90be09ba1f0df5effe172d79e5eca8b519dfc3585a326bb1a8e970fe103b5e44bb2e9b217713e8d02a9131ddb3bb323139423c62ae32224fb6da9894c397e4a4d88868b242b08ac7643ed9c381ed83c1a131c7d53cee7da94064f2c2866652827310f96d52a6bf341f9374e5b48bb3fa520abee80cb2d1906a8cd5ef070255297004dc389d2d4739d0f5741d234aa595c7cea5274d732f0c562e14e4db08024380b2bf131bba75682dd378d136e2e8a1a29a521af5f9631a14d09c5d9965b9e7d3151d0ca3f638ba6c16de16762d66ac2dd9d27abd588d98af348ee53dc787a7bd16e9110adfe2ea2f203ce15eed545393f0e7d824ad925c7ee4000bb3fc3c679b13702cb789e27428fcf6af0462799303d24b2b4dc5212ed356933c52077eac0927b4c9580e78cd2fadfcd05789b664ec40ddc92cbc19c2911610a5d31eaa29ea8ffaaa63b72cc01b4d78657c1efb23a711f072e5708938f2270f6e9e35c0de97a3155ac7b855110ffb6eac717ba24b8edffa4a2970b9bf9bc8a84ce3959341a921f6477a4004b90797a97c6eb3af5b73a3c5c777388bb667f8f00829df7032c42e8a279cd2986b9f92e997c3fa9ea2d299120ba7b0e9e3951c50c55db9570899b0076f157c2242602b83c2736951ed2381bc74ef84b52de3736e60c92bc978dca9155c5c2f25d409a5994d1d9df95d580e163baa4cd276434296f0a3e5286d9ceeaa1d8834284366b7b10b80c341a0d24608c6a09e3e09b03db789e3f8157e4af90bf581b833b1fae12c63335ef58787600577e2fd6f7363dfdd79fd460abcc98792a40e84f2000a0d77c91e95cbccc671e77d06a69d27e682cb4b50e2f94d6f5f41ff67cda3b1da86efe0c0f9fe38b247ca7f1a8f4e6c44aaed96679b7a1a20567e07772247876f21d598b820606f73c0860bf7410194675fc65b756dd7ffa9b040dbde30678cdf074b8f679739d88dffb95f010502502c9bc5ddae593a1b784efe741561a4b744da51505d7b77ae79f825863eef160c2954e4f081f282c404f515b7379869ba0aab2b9beccd0d7dff6151e243039565f8690c2cfe2e6e9f7042f3b4f5f71788182a0a5acb2c6cbcecff510181c4a57646586a0b4ce000000000000000000000000000016253742'; -export const SIGNATURE_MCU = - '4a48f6a40108b2ba5aad73cc09ecb304430c8ddd7b8170d1f16007de8e2f1c650011fd507fb0d24b29382c4c46d629502bfdfa5db8bc64d508f2f183a76696c473063daa906d55c3fa4d2906fa1a0487a96f5090f2ba80a90a8b7393ac761b6bf874f1dac821b6751c9865609d5541f8c59e4bbf4b8bb643a3da2c083fcaf1218f3e32bc6363ee681a3266eb09928f591cf3ed22ded28664eefe0effb5b4f21c5bc6f7af4b506999b35d4617dd98cb08003330d486bcd65d0ca335e4a3412db59095e7a0d6eb8fe9d6228849fdfae568f5fdd7338812264f9c750ecd07f42649e49e59015127f9249f334f2040f7bda1428a0b0d1c911e80d4e78dd4717820827f1248b4ee4127f6e3cbacf0eb37239db80d41a713db0d6706b8b541f18bd0e1dfce69b67e91ffb7630f5b5bf96c363ecb867edd396a3686c7d6282e336c2d3142168823b7ec1a71eef6f3897541d5dd539894b98fcae4b7cf5f10a90d2965aec9492fadbf49d1cf94ef308efd95f8f78dbc90d92ec402b9da5d15359972e74d60491882febdf75ffca2db289a02a6ba020a7ed2ade1cf0555df7ad52964754d0aaa1957caa37c50be67f63698461166042c8be25e41515bf9b9b1d6d7b8ffbf275769bc42bcdc9d29c973f15f3c10c0d896c3ee171fcf86b3939178c4308b1ccb2a620557caaf1941b3afee6958b53680be9d74ce89e5fff17e172db6b76d78c331a6729b7dc892a3001975e30f3839b9782ac88e43ec4ca27c3c297af88edfbba9e4c5a9bcb4e3f115cd9f7617a3f7300641e023541e33e345a5aa13dc32d38853d49270c7d54b2e1e81914e2cd507d288b34be7f6c80d27649f7e428ecb29a5e686081185a6de9fe571dc6fb349cf9bb24a57d4a4c8d356f0cf0a53be2f55f5c3743d140b267e11bc22e90a9483086850e9d75882fe95fa5973cc10a8e98c6c792f5671311a57e630ad1cd9462e9a39949214d92adeab7eb0272624c72e068c2292219fd9defc0a206a56e4801fe31774a18fe836892316eb720cfbb96ec144618e72ba556177e2a38fb685253afef2b0a02d4749492c5ccc9f529014d6d77ac33c5653bddbec3e471c385aeae69952bbf783e2bc38b0c3b0e6dc910b228b83164411cd65b5c98ebee3ca4063de362a159b43348fdc49f3092f4b4c9521ab4c8ec86a1067c12020181ba5bca6b60dc2e3b34c67d67d33c8603358d0d4dda666fb3a4682de2f7fbef3564cc3e75b26fb89222696f9bec3e90e9f62ad8649595165017e2ce7a267822609c28126a324e9045974078943bd21a35097a4b1dc968db25b66c7e83e832a37bdf664f5a478b3a366f36375762c34e3ff828aeb9c604bd82557007198d3a3fa09f8b569b31451e890271fe0356d4e4ab1cb9ad21d9fb713e95d0342fbc945d46e74379644a02b963c9ace3a5904015a7413cbaa11b27c24c77c8cb0345ab9685801ceb249b8ffa666bb87784cfb0b638fab84c3728790dd0293d7b1d1c877518c27a3a14ef57d4ac9e6f34ed3f4933222574833100bca16a7b0d72c54a62c40e0fda92ba025fbd6d9fb46fcf4e6144f680d2232189e307e496a9583d5822027a50ea11dfb8a8e6c1c85df00ad730d4d5fb32969e7bf06dcef83e5284ca9680894a7530c4ecb20f6de8c63824774ce9227c96fa0182b972869ebe23f11fe357c5d39d7be65c24be900e28aed940912c01770e66604968a5313cc24a47a31dbefbcc2ab208b4b8f4782d588d57e757dc3aa4f9ab4057c58977a0ac13c422e2b56eef39f8f08f6b19bc25cbee47a4841114d537fa3d2e8a5dc0a96b49b39ccf9fe7a587a02860cef30477d4398f9cb353c499d3751f73a7b955b0f89701700af3f0eee742150b9f9acd4520d316a97dcae11b705cb619582da1b621efb91bcb2c6e4ad0518e1b03e4b4a4bf12b0af7c630de012df80061a5a0ee12fc022e4532b489a7b0cb6012a25a0b3f158711c500dbaddfbde6e5822878b527d9802b274b2a9bb13699e3739ba0216dc40b798adb4789259d9f1212e07c4755595a91a2fd7cc56aceb528530bb21b5116b87dee47950f55c4a1d4f5b235eef954ee6bad03ce32e6829872de0cc9c60fb6dd252265c56b793b643b926acc50156322ad591898e5159555c9536ec47ace674916a19f19d9a026ab77662e0fd2dee3614ead82514054ccb1f810589debe3188d5e9c07bcccc943f1146a221a65f0a2842552529aaa2255fedb39805e4c00f3f8cec3966561f827aad0ebd41598c61ced9274e50f6bbc70c41986e315df4121406842b3856c4ec0c0af7e189142082608ebc5a1f2070afe4e88a4d782efbc01533ddb606bd6d2415b575f0027cb870e339f212e80a5d606ec3d416c37ddfeadfdc48fdffa88a8ebcc541d381b69ff51bae5f3f23e463196d0f9b03f865910028b4b32a9b114b130a2a9499452b9c2b6e00fce1b3c9514a5f9e1910d0aa783b5c0a8337dc114d87402d278c659afb4127ea9f09dfe6599b8ee4dc19d17bdbb40801f8d2f27fa2fe97d1a16a085c33098f023efa9428c3cbb897facde4c71f15874712c054a5f30c21f1f376529476b8bfb48a3029d8e9b684ccae66ca7c743c2c58cd4da5e8a9ac83bc9058c5258edfcafb1b6eb51b62b693f37ea5a36ce375d2cd6333868231ab7f5ef507089dc8f0fae8ffc5fdfee094c3e3b20c9eee1f677de869a0c5d44de148acdffe6b98a7ba5e7fdbd5ae4c2284d756135e8aa08fe912cd343e04cf36a43a24c97ff162480e660ecbdd09a3e77dd674c2e4949a5a87f93a98a209bdc37b94beafb2f911d2bde7a59685039553f6e2e44cae79ea766a53819926eec81070a988a234b45b50633158f890f5595c11c3162cef97c2cefdb439af0f86909091ea16590b6bfb20a3d23ea86ad04b11b7d4351ba983926664c3b0d75ca2c1f79e57b4a30bb8243fe7fdb773cadd9b954a2c8fe54d1d0196292367a3ba1396fcb59c4c3fd3301ded699a6144124e14b2a737b334cf29d3e9b644f3971b7defdf70b88bcec4d3adff7b1d93f5c77ae8971190d27ed8f43c5970d59feb4eaa4ffcf2c2a5060e78f697b760cb290b5eb6eebb1db7c7d7ff62584bb0ea1a806c1e06eb525baee7447e4efbbfbdc3b742def705f66d4fba9d4c74a33eef2f623eb34d6312396650bb0df2d82aa587c61a32860757bd8d2a7ecf037d542c9a2fcbb7c1b845013cbf0663a27f201a33fa2d0dfea978ad3c07c0d5d8df2163363d59ffb11d616d95103058b6cb52eed6d2f00421e63cb14b68de182a39d04d9ce909ee8778d037b3920356a6d909eaacad5d8e5f40f30373f4c777f96b4c7d9ddf00a0b232d2e36424f616c717898a9afb6bac1de090f20494c75777c838a8ca6bad1e9eef3000000000000000000000000000000000000000c192c3d'; -export const T3W1_ROOT_PUB_KEY_MLDSA = - '1f3e5edad45fc6980dbfd597ed17b77e4d4ad1b1ec4804e04dc061cc2cfe77bc96fbc93f698f1fe00c47177c8150276dab86cbf407add7527deb09b65a6623ef581422292d77744be64f4401ca65b9bd027e32a76f307413b086869eccbcfc5e8823be8e0b3897727bca568feeff62957cd1e261e4aab67271725d240c7e7d2a20a5c201699be83d6c6a57fdfc09ac9b6b599e2a14a50e421d246d1d1df805db0b25534e500fed76fa89975a4270e274a9fa72b528e47d999abdeca5e08e51d447ae4019f699088d485bb52d5eec7a8c725cd34ff955c16437877a131f068438b5699c2285000463e24c633b023cde279be015db2f8b16de07fa82ee9adcbf868b898e25eeef5d60d03e1da874aca6f2089b1e7c72d7b532d3a27e38936d53c0080f9908b6727b13fe961ec8b08004f077b9cce95b5eee9b44d351e79eb222aba1bc18895d36cf4843ffcdfc735e1b66f3e72db5aaf8b5ecf56dbf0d616cf53d192a60e61ad3c51a595213100fd00835434a28aab3eb5949d3405de5db1a586bd6e937444913e9f1d4c00247acd49a2ed9953636e5d74417b4c7144c6c22fd8f7e25cb6cf6c8510de6915b74c95a28c197152208096d0249b86150e807978ca985c110d07b7d0a24434039f6ef12a69d3e073af6ae44e7c4606a354a3e953bdcff10c8dc68b807f41ac563ff0d7141c328b34152a25a16e9cb8228f7707356d2d7f3caa8d778a85e3b66242c124281c0f3201ad28596b868db138149eb98b821dcaaf86b5e11c2ac1161975bf0b2ecefc111b7b7401ce261911526618044d126eeee885efe51be0e32e93aaaffe34899df5b10df7c5a9c54c5b4704671640a92e8631f0d401f44c2238b5f9a37499b3d83c1c60096f2b48c3081dd9769db325c1cd1ffe84bfd8a80dde3cd650c49e14af3ab6c317ee74bd39e976ff5b7ad490e80f3eff9edf3b3855070a5a06fbe9cbca54f521e3d74cf13a4d116e23e531a7c70232c044b7e7d02467afe2a40def14298688eced5d3d87a102d31d7cd5409918ced4bab190505df48f451deea1c29168246d621529f0346eaab0e2302f3ce206dd84837704b8bdedbadea5b5822cdecbfdb180cde14e62ee26a929f0a37a99bacfe237376a3e6c289b06b8e93ed9d1435e3ee6399ab1a06d4f70097f80e26fe6f60b27a68104f6333b8db1bc78e12d54647b23feb0f11ae070f3a6be9d9f4d89a6b5ef8595d010d410e72138cfcbd5e41cca1df33ddad657b699cf4d3dc602888e5aaa0a7a2325ba4a36d7de49c591d8e355fd1de644d772bf2a060093743388dbe5d30a2d97304abc17c547bd65276291d6aa7b0723703733239fabef39c2de47071d50074e68fbe71c88f75d4510bb5f6bc244b3e7b219eb89fe5f148459a2d693cd3fac32aa069cbdd4e27c9778096f6805bf6df9aec9671f0a58b28704f74775e81ac46826c1c4709b0e0989d6e8e73bd1795ce85d85c14ad2997cd2810272a5682e7ed9aa7a092708c0f4bc67f68d317d406a96ddd2842ea22cbf3410cd7a77005b61115b794894cf9d10ebb4f5552bec8c21d90ba97d6139d5261d7f51f4009c3618f9feb22cc929ade204eca7c0f813409ceb88a13512f85e03948314ba9c4488aafda88ce447a0e052a6a605e32d04a20e2e52c41f371247963f24fc28c5cc1ddd8fe1681a2a293daf2688e9b026303a76cc60f40290c4d823efb85f36cb5e092369cc20515a8077e7702ed54ca1565d37b83184ee46c4a6adf2840f5a5fb002db70690bbf93888e685fdd1f9ea211b42693e0bd5ba5235a6ea1b714774d13279d7db3440f7fbced53f203ce6500555bcb9251e6f36d3352326501c'; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts index 39b70d9de..11fb5b522 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/__tests__/verifyAuthenticityProof.test.ts @@ -1,15 +1,7 @@ import { authenticateDeviceFromProof, getRequiredDeviceAuthenticityLayers } from '..'; import { prepareDeviceAuthenticityData, verifyAuthenticityProof } from '../verifyAuthenticityProof'; -import { verifySignatureMLDSA44 } from '../verifySignatures'; -import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'; import type { DeviceAuthenticityConfig } from '../types'; -import { - CHALLENGE as MCU_CHALLENGE, - DEVICE_CERT_MCU, - SIGNATURE_MCU, - T3W1_ROOT_PUB_KEY_MLDSA, -} from './mcuFixture'; // Golden vectors from trezor-suite `mockDeviceAuthenticityData.ts`. // These are real device/CA certificate chains and challenge signatures. @@ -103,36 +95,11 @@ describe('verifyAuthenticityProof', () => { expect(result.valid).toBe(false); if (!result.valid) expect(result.error).toBe('ROOT_PUBKEY_NOT_FOUND'); }); - - it('verifies Trezor Suite’s genuine MCU ML-DSA certificate and challenge signature', () => { - const result = verifyAuthenticityProof({ - certificates: [DEVICE_CERT_MCU], - signature: SIGNATURE_MCU, - signedData: prepareDeviceAuthenticityData({ - payload: Buffer.from(MCU_CHALLENGE, 'hex'), - }), - deviceModel: 'T3W1', - config: { - version: 1, - T3W1: { - rootPubKeysOptiga: [], - rootPubKeysMLDSA: [T3W1_ROOT_PUB_KEY_MLDSA], - }, - }, - }); - - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.rootPubKey).toBe(T3W1_ROOT_PUB_KEY_MLDSA); - expect(result.deviceCertPubKey).toBeTruthy(); - expect(result.serialNumber).toBeTruthy(); - } - }); }); describe('authenticateDeviceFromProof', () => { it('keeps T3W1 Tropic requirements independent from root-key configuration', () => { - expect(getRequiredDeviceAuthenticityLayers('T3W1')).toEqual(['optiga', 'tropic', 'mcu']); + expect(getRequiredDeviceAuthenticityLayers('T3W1')).toEqual(['optiga', 'tropic']); expect(getRequiredDeviceAuthenticityLayers('T2B1')).toEqual(['optiga']); }); @@ -228,43 +195,6 @@ describe('authenticateDeviceFromProof', () => { // even reach the tropic requirement; either way it must NOT verify on T3W1. expect(result.verified).toBe(false); }); - - it('requires the MCU ML-DSA layer on T3W1', () => { - const result = authenticateDeviceFromProof({ - proof: { - optiga_certificates: [DEVICE_CERT_TROPIC, CA_CERT_TROPIC], - optiga_signature: SIGNATURE_TROPIC, - tropic_certificates: [DEVICE_CERT_TROPIC, CA_CERT_TROPIC], - tropic_signature: SIGNATURE_TROPIC, - }, - challenge: Buffer.from(CHALLENGE, 'hex'), - deviceModel: 'T3W1', - config: { - version: 1, - T3W1: { - rootPubKeysOptiga: [], - rootPubKeysTropic: [T3W1_ROOT_PUB_KEY_TROPIC], - rootPubKeysMLDSA: [], - }, - }, - }); - - expect(result.verified).toBe(false); - expect(result.error).toBe('RESPONSE_PAYLOAD_MISSING'); - }); -}); - -describe('ML-DSA-44 verification', () => { - it('verifies the exact message signature and rejects a different challenge', () => { - const keys = ml_dsa44.keygen(); - const message = Uint8Array.from([1, 2, 3, 4]); - const signature = ml_dsa44.sign(message, keys.secretKey); - - expect(verifySignatureMLDSA44(keys.publicKey, message, signature)).toBe(true); - expect(verifySignatureMLDSA44(keys.publicKey, Uint8Array.from([1, 2, 3, 5]), signature)).toBe( - false - ); - }); }); describe('M1: CA public key blacklist (revocation)', () => { diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts index e9e642038..bb447abe1 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/config.ts @@ -1,5 +1,4 @@ import type { DeviceAuthenticityConfig } from './types'; -import { T3W1_ROOT_PUB_KEYS_MLDSA } from './trezorRootKeysMLDSA'; /** * Trezor manufacturing root CA public keys, per internal model. @@ -61,7 +60,6 @@ export const deviceAuthenticityConfig: DeviceAuthenticityConfig = { // upstream: DEV_AUTH_ROOT_PROD_BACKUP_ED25519 '5612606584ee7e0bc313b13f7ac94156bb4cb75bd77585ddbe579301306e85f1', ], - rootPubKeysMLDSA: [...T3W1_ROOT_PUB_KEYS_MLDSA], debug: { rootPubKeysOptiga: [ // upstream: DEV_AUTH_ROOT_DEBUG_P256 diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts index 80a1c2025..16456f637 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/index.ts @@ -25,7 +25,10 @@ export type AuthenticateDeviceResult = { * the explicit model policy verified up to a trusted Trezor root CA AND the * device signed our challenge: * - Optiga (all secure-element models), and - * - Tropic + MCU/ML-DSA + matching serial numbers (T3W1). + * - Tropic (T3W1 / Safe 7). + * This mirrors Trezor Connect's production authenticity policy. The raw MCU + * fields remain in the returned proof but are not a client-side pass/fail + * condition. * The reward backend must independently verify the same raw proof under its * own versioned policy; this client result is only a UX preview. */ @@ -65,15 +68,11 @@ const modelConfigOf = (config: DeviceAuthenticityConfig, deviceModel: string) => export const getRequiredDeviceAuthenticityLayers = ( deviceModel: string -): readonly ('optiga' | 'tropic' | 'mcu')[] => - deviceModel === 'T3W1' ? ['optiga', 'tropic', 'mcu'] : ['optiga']; +): readonly ('optiga' | 'tropic')[] => (deviceModel === 'T3W1' ? ['optiga', 'tropic'] : ['optiga']); const isTropicExpected = (deviceModel: string): boolean => getRequiredDeviceAuthenticityLayers(deviceModel).includes('tropic'); -const isMcuExpected = (deviceModel: string): boolean => - getRequiredDeviceAuthenticityLayers(deviceModel).includes('mcu'); - const matchedDebugKey = ( config: DeviceAuthenticityConfig, deviceModel: string, @@ -82,11 +81,9 @@ const matchedDebugKey = ( if (!rootPubKey) return false; const m = modelConfigOf(config, deviceModel); if (!m?.debug) return false; - return [ - ...(m.debug.rootPubKeysOptiga ?? []), - ...(m.debug.rootPubKeysTropic ?? []), - ...(m.debug.rootPubKeysMLDSA ?? []), - ].includes(rootPubKey); + return [...(m.debug.rootPubKeysOptiga ?? []), ...(m.debug.rootPubKeysTropic ?? [])].includes( + rootPubKey + ); }; /** @@ -132,10 +129,8 @@ export const authenticateDeviceFromProof = ({ }; } - // 2) On T3W1 the independent Tropic and MCU layers are also required. - // Their certificate serial numbers must match Optiga's. + // 2) Trezor Connect requires the independent Tropic proof on T3W1. let usedDebugKey = matchedDebugKey(config, deviceModel, optiga.rootPubKey); - const serialNumbers = [optiga.serialNumber]; if (isTropicExpected(deviceModel)) { const { tropic_certificates: tropicCertificates, tropic_signature: tropicSignature } = proof; if (!tropicSignature || !tropicCertificates?.length) { @@ -149,32 +144,8 @@ export const authenticateDeviceFromProof = ({ if (!tropic.valid) { return { verified: false, error: tropic.error }; } - serialNumbers.push(tropic.serialNumber); usedDebugKey = usedDebugKey || matchedDebugKey(config, deviceModel, tropic.rootPubKey); } - if (isMcuExpected(deviceModel)) { - const { mcu_certificates: mcuCertificates, mcu_signature: mcuSignature } = proof; - if (!mcuSignature || !mcuCertificates?.length) { - return { verified: false, error: 'RESPONSE_PAYLOAD_MISSING' }; - } - const mcu = verifyAuthenticityProof({ - ...common, - certificates: mcuCertificates, - signature: mcuSignature, - }); - if (!mcu.valid) { - return { verified: false, error: mcu.error }; - } - serialNumbers.push(mcu.serialNumber); - usedDebugKey = usedDebugKey || matchedDebugKey(config, deviceModel, mcu.rootPubKey); - } - if ( - serialNumbers.length > 1 && - (serialNumbers.some(serialNumber => !serialNumber) || - serialNumbers.some(serialNumber => serialNumber !== serialNumbers[0])) - ) { - return { verified: false, error: 'SERIAL_NUMBER_MISMATCH' }; - } const deviceId = optiga.serialNumber ?? diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts deleted file mode 100644 index defaa75ac..000000000 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/trezorRootKeysMLDSA.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Synchronized from Trezor's public @trezor/device-authenticity config. -// These are public verification keys; manufacturing private keys never leave Trezor. -export const T3W1_ROOT_PUB_KEYS_MLDSA = [ - 'ea67d6000e79d16bc9119cea88b3415369d612f02a6bca6e18e0dbe5e2d37da716b993a05258a642c656a4e1c80f2db1f2818e5ebcbe5715448389079526895f8e620001b1c1dc857aa6165ec1d0b731127c424cbf0dec1ae33bbce500b9e7974d2a83626fe0be91555d79e1f91fbcb5df821738385cba29dc1a71c3ef2d1d3b3165ff66b4638b7121db9ecdfeb39d20163732d1685f7604956de9f549c5a7fcbf2a27b913a94107904cdbfe486df4a62dc27f0f7ba14c1103f9de86480c12bcaf1bab70584277a9fdac50d03d50e9e7bb6dd3b8b61cafe07c5513c4fbb35d14766fef0582ca1d78ebc4fb246ee3ac231eef770e74ad4207dddc8f3666700c84aa1446cb5ceef0b6f113041a9eeb8708564a4c43fdb46203dd1a2b2c48a0e485bc10a72a8a8e5b006d3d398941b33365b04b54d14d230950c7df048afd2c16ff1515387a97b712f68f65f08fd722d4b75c97007c4f7dfff5e5f22908b56bf40acd5263cc419007a355bcd1f4a3dd201286e58be1165e0eedf6c236c7aaa7cff3a8fbd6aee5d3ea5368bbf0aad8f41147ce874e2c97ed3fdfe859def143f63c1e7a093ce18615016a857e875a4099d7a56b036c7bdba205ff21841c4693ecfe08ae8faf10c73fe3a8e202a585c39ae8b084f2879a2c20faf8abe5163eb8acfffde4b9a73a9706bb5d3ae91235f673df8f20f5dd72c06df8a7401924c614eb12d915deffacba689738612780877f39a891b3cc0673c728653f72874386d6a097516d56c196c9f1515a62b5ddbab2c795d48be8b6a304533034f323c6970d43a08957d8b0db7cf80e6c02589069f9bf622e60f6953bfd10d68cac3d421bf9145169fa0b10d2384212556e042d95686c44bacd01b8b2e698163ca6d2b965557e8e16f0d3672bb32c0dc3bbced78f75918df59742fcb9834925b50c568ec305db53e3d7915851620f0763820ad511896041d7a836b764b51273f6ab1e4021e67bcc5095c02f55c132b674d47b36271830508c6d08e7578181fcf95c9027c43bebc56b0d083d6bbde10e70113ae41102181f147bd6aad58898d296b14ec4f7e049ff47831949b8f5aa29a2c74e7ea26a4270e84d4ace36407b255767f9eb9676d15559d1c403b1a5949a9ca2a74e6e0f2719b11306911799be9dea597ec8b375bac646f8a9a03e6f2271d9fb5b695c51d7fe2125a08e243d63e5d426b6dbbc149a87e65f8b1888465f99ce519ce4dd8d51432e45e4bd01a41893189a8656bc9c9226fc4bbc38441569d3be87180c833d63d53adc15d953ec6fcd1643aa00f20aea7cc169efeeb962ebb724ba0008861a1fda988c93d3ea0e3224d4c72c0c2fecf4f954f9fcf47b5f39b59a9c4c12423382ae2ef547310490bb3d57661a51b9ee356d0720f6e376e2374c38e738e158dbec8d7cc5a23a6269a3ca441a4ec406ddeef5a0d6007d0b3590ba19eeb2a6da67fe21b68625d1d422739c928c7b9a49163a6ffe60cfce4cfa918b690fe5b300022e88ff164020ce9815d18c97a63ad3383e1101eb523c5e1d20482dd6d0411b13b75c89e864fa6bede2674bfa5c3283a1487a3093ff05dba3f86420a7a9ac62c59971fa9d10c9ebc68f36d65e798f95c3a2cb11993ae1454855cda4d8290f2f0aa8f48cbddad92211a1e1a38588822159c257c077a7bd1c1afeb3644e6001a1f0dc0833993c068879f484d6a4da8c1fb85959d282be52583225b60c31cf1ebe0e74bbd3fc74caf039ad3c03f59b5da694fa810e199ad7587b593548c828a18c6a9b657df6cb39e839f36beffc07c6e85c8bbd7e98646af879bead4d3e1b79c401eb6449a4200f1cbdad39329fc43c5553a286a0', - 'fedf8b0c16164b9cec427882aa03d3bd6a1588cfbaebaf93cf2fc52f275417777433a938dcd6516345148f728c935a40ef3b6c41fd3ed831fea13774bd39887da8b06e2e671cc32a1c72e44a13d1cd985dc2ddcb63e457bbd29d38186ddb776b8e73b645c8bf616309f890d175538a9d10c82e595dea03f70564ff69b53497ecefd51fd8320041d9b1092386033ed37a93dd9b4b9da1297f808eac21020b8dc4747e01854ebdc407a04fc18b2758720125acb41f0c60e16cd53707a65392feed06c9fcc28a3e738dbc91614e89e5d535274c6b226d83f335a0dd088755571cf62496ca4642ec8e65eba8be2b8cec46416c6a3ad4be10569ac532e02d6e06b6ce0c0d4b7a99a11c910f9f0631b0cd72e20637237eeff723e722f7aa626732434c47843a099739e96e31267d8d6905d8b2b712a5a69d9343a3c1e07afb414b73950f4b1df196243005fb61c6200ef346826d6c0f4cc8ede052ed48235997bb366dade4483bfc11fe65fd0c5539e89d8bd271e54e30da6e5192a0f343dea3806aa798d12d62251a063d071ba67c35d3a26e582c2d361cf2d0fc36fb1b5a46e469669338b1c1744bdfe365700a6a1c641ab2b7906e2b7c0c0a9af8862ab5751a678e3e0156847ff6093ca7ebd48dd8b1d6f7736c2039e705c1e20f42ed5cac6db3dc7a7e2facbb75411e7fb8a156df52a2926afa9070937cde4dd4bd456294ba59bdcf123f11dcda267d475721abfecab1b31848a27236aa751e5b55015f033d87fb9cf5c086e09f1667cf7ec2e72436c907c5763690d698fffa997709bb89526cae772bb962c06ed1b89d418835672b85e1a1b2e3c55f4e607ecad09bdb3a18224b9805659aa2ecf0b62dbf6265e27d4101450edb590d52d525b62b1114dad2eb06c951a2c77c7ecfbf5f1f6fe285f39727958ce0b5cfe8fd465420c270309999e6694ee31fcb7d6480a75d0d72558cd6a050b8d11a9a6d7736700027fbe14b48949b7e1af2ca148ad4e4b4b93e9575a4119afd1b57c250606d5a4e258c2393c104d651995b1e75887f492ff05e92e1630332f176d86a0453b412d0869d98d6ab27377a74766e3cadde29c1853e6398a9edfa48c0d14e7c148b048f1bf25d11979e94878cb86eaf1e55638c1b39e0ac5254a009bf957f4b624796753c3451246e5a7b9022926aa77b7a1b4ae7d25f776d4395ba98a37d9e21b221da29564e50a669056650a40489386e8909c6ca21d369c3495c44b2f71373a392c9fa89c9085f467d24d6219fcdd2b56126792eb6a14269c4264a56926b1a7d9a1c03d9649d157f938abfdc2a56c20795065774838030da41a4c6178919cc1c6c88de1458d9baa1e51a4a1994e6d5cc94b049b15c235c5cf46d2cf23f09d565c5a5497ef0b3a5fbc8d1be8ed78f3794c49d4936139b0419faaffc52b55cb91f6e1bafacd55523c0b7eec56d98e4228cd0f9d00bc9866dac71e43db2b2cf311999d7bc3a49219836f558cbb5d8faa79e83079c6ce92288aae852553b655ef6bc87fbb25ca3ab482aecaf0a86eecc92341906cc89d234087f5abd6d2100a60d361d516d460d436b46706ad44b8b4ef3fe54ea198ffc2cf8c8e144ec62c4ce86ed5d181283fc17c926668ca4e0b8683e0c7bc662a54ea7f4c830365ad1cdea58abee9a726168ce9870729972884ecfe5baf06f1ee664aa422e26f9f76e20908d72e78c1a2dcd94fe1c07573d7706d7bdc07d0b41498c0d52ec3a90d6e7c0fcef597a884fb2caba29c2279569a1e64dd6c7ed4b312d2197b6391407899b25d21cefd1408eca42204f76cb0436f44c0a9c2e1eca19df8d93255c0da6469dc47da883e186387f76011cc8', -] as const; diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts index eaf4c70a9..56790f439 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/types.ts @@ -1,7 +1,6 @@ export type CertPubKeys = { rootPubKeysOptiga: string[]; rootPubKeysTropic?: string[]; - rootPubKeysMLDSA?: string[]; }; export type ModelPubKeys = CertPubKeys & { diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts index dda557dd1..09af84f62 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifyAuthenticityProof.ts @@ -60,18 +60,13 @@ const getRootPubKeys = ({ if (!modelConfig || typeof modelConfig === 'number') { throw new Error(`Pubkeys for ${deviceModel} not found in config`); } - const prod = [ - ...(modelConfig.rootPubKeysOptiga ?? []), - ...(modelConfig.rootPubKeysTropic ?? []), - ...(modelConfig.rootPubKeysMLDSA ?? []), - ]; + const prod = [...(modelConfig.rootPubKeysOptiga ?? []), ...(modelConfig.rootPubKeysTropic ?? [])]; if (!allowDebugKeys) return prod; return [ ...prod, ...(modelConfig.debug?.rootPubKeysOptiga ?? []), ...(modelConfig.debug?.rootPubKeysTropic ?? []), - ...(modelConfig.debug?.rootPubKeysMLDSA ?? []), ]; }; @@ -130,49 +125,9 @@ export const verifyAuthenticityProof = ({ ); const firstCertAlgName = parsedCertificates[0]?.signatureAlgorithm.algorithmName; - if (firstCertAlgName === 'MLDSA44') { - if (parsedCertificates.length !== 1) { - return { valid: false, error: 'RESPONSE_MALFORMED' }; - } - const [deviceCert] = parsedCertificates; - const deviceCertPubKey = Buffer.from( - deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes - ).toString('hex'); - const rootPubKeyMatch = matchRootPubKeyToCertificate({ - allRootPubKeys, - cert: deviceCert, - }); - if (rootPubKeyMatch === undefined) { - return { valid: false, deviceCertPubKey, error: 'ROOT_PUBKEY_NOT_FOUND' }; - } - if (parseModelFromDeviceCertSubject(deviceCert) !== deviceModel) { - return { - valid: false, - rootPubKey: rootPubKeyMatch, - deviceCertPubKey, - error: 'INVALID_DEVICE_MODEL', - }; - } - const isSignatureValid = getVerifyFn('MLDSA44')( - Uint8Array.from(deviceCert.tbsCertificate.subjectPublicKeyInfo.bits.bytes), - signedData, - Uint8Array.from(Buffer.from(signature, 'hex')) - ); - if (!isSignatureValid) { - return { - valid: false, - rootPubKey: rootPubKeyMatch, - deviceCertPubKey, - error: 'INVALID_DEVICE_SIGNATURE', - }; - } - return { - valid: true, - rootPubKey: rootPubKeyMatch, - deviceCertPubKey, - serialNumber: parseSerialNumberFromDeviceCert(deviceCert), - }; - } + // This mirrors Trezor Connect's production policy: Optiga uses P-256 and + // Safe 7's additional Tropic proof uses Ed25519. MCU/ML-DSA is not a separate + // client-side pass/fail condition. if (firstCertAlgName !== 'P-256' && firstCertAlgName !== 'Ed25519') { return { valid: false, error: 'RESPONSE_MALFORMED' }; } diff --git a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts index 85e16e34e..0e10efcf9 100644 --- a/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts +++ b/packages/hwk-trezor-adapter/src/deviceAuthenticity/verifySignatures.ts @@ -1,7 +1,6 @@ import { ed25519 } from '@noble/curves/ed25519'; import { p256 } from '@noble/curves/p256'; import { sha256 } from '@noble/hashes/sha256'; -import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'; import { type AlgorithmName, fixSignature } from './x509certificate'; @@ -34,17 +33,8 @@ export const verifySignatureEd25519: VerifySignature = (rawKey, data, signature) } }; -export const verifySignatureMLDSA44: VerifySignature = (rawKey, data, signature) => { - try { - return ml_dsa44.verify(signature, data, rawKey); - } catch { - return false; - } -}; - export const getVerifyFn = (algorithmName: AlgorithmName): VerifySignature => { if (algorithmName === 'P-256') return verifySignatureP256; if (algorithmName === 'Ed25519') return verifySignatureEd25519; - if (algorithmName === 'MLDSA44') return verifySignatureMLDSA44; throw new Error(`Unsupported signature algorithm: ${algorithmName}`); }; diff --git a/packages/hwk-trezor-adapter/tsup.config.ts b/packages/hwk-trezor-adapter/tsup.config.ts index 7188afd3d..b812e199e 100644 --- a/packages/hwk-trezor-adapter/tsup.config.ts +++ b/packages/hwk-trezor-adapter/tsup.config.ts @@ -15,10 +15,6 @@ export default defineConfig({ clean: true, sourcemap: true, splitting: true, - // Local debug-watch copies only this package's dist into app-monorepo. - // Bundle the ML-DSA implementation so that flow works without publishing or - // separately installing a new transitive package in every client platform. - noExternal: [/^@noble\/post-quantum/, /^@noble\/ciphers/, /^@noble\/curves/, /^@noble\/hashes/], external: [ '@onekeyfe/hwk-adapter-core', '@onekeyfe/hwk-trezor-protobuf', diff --git a/yarn.lock b/yarn.lock index 05c6a22e1..e7075eaf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4972,32 +4972,6 @@ resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== -"@noble/ciphers@~2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.2.0.tgz#84fb45ac9332925d643b80f89ceb0ea2f21dba95" - integrity sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA== - -"@noble/curves@~2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.2.0.tgz#981be3aadc3bbfbcdb245e78cc97aa6f759246c2" - integrity sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ== - dependencies: - "@noble/hashes" "2.2.0" - -"@noble/hashes@2.2.0", "@noble/hashes@~2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" - integrity sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg== - -"@noble/post-quantum@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@noble/post-quantum/-/post-quantum-0.6.1.tgz#5db8d341cc0335834df185f1fe010782a8c9a1fe" - integrity sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A== - dependencies: - "@noble/ciphers" "~2.2.0" - "@noble/curves" "~2.2.0" - "@noble/hashes" "~2.2.0" - "@noble/curves@1.2.0", "@noble/curves@1.9.7", "@noble/curves@^1.1.0", "@noble/curves@^1.3.0", "@noble/curves@^1.4.2", "@noble/curves@^1.9.7", "@noble/curves@~1.4.0": version "1.9.7" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" From d546bc55410990351d34009230aefea77725d0d6 Mon Sep 17 00:00:00 2001 From: ByteZhang Date: Thu, 30 Jul 2026 11:17:46 +0800 Subject: [PATCH 12/12] chore: bump hardware sdk prerelease --- .../electron-example/package.json | 4 ++-- .../connect-examples/expo-example/package.json | 10 +++++----- .../expo-playground/package.json | 8 ++++---- .../connect-examples/hwk-demo/package.json | 18 +++++++++--------- packages/core/package.json | 6 +++--- packages/hd-ble-sdk/package.json | 8 ++++---- packages/hd-cli/package.json | 10 +++++----- packages/hd-common-connect-sdk/package.json | 16 ++++++++-------- packages/hd-transport-electron/package.json | 8 ++++---- packages/hd-transport-emulator/package.json | 6 +++--- packages/hd-transport-http/package.json | 6 +++--- packages/hd-transport-lowlevel/package.json | 6 +++--- .../hd-transport-react-native/package.json | 8 ++++---- packages/hd-transport-usb/package.json | 6 +++--- packages/hd-transport-web-device/package.json | 8 ++++---- packages/hd-transport/package.json | 2 +- packages/hd-web-sdk/package.json | 10 +++++----- packages/hwk-adapter-core/package.json | 2 +- packages/hwk-ledger-adapter/package.json | 4 ++-- packages/hwk-ledger-connector-ble/package.json | 6 +++--- .../hwk-ledger-connector-webhid/package.json | 6 +++--- packages/hwk-trezor-adapter/package.json | 6 +++--- .../package.json | 8 ++++---- .../hwk-trezor-connector-rn-ble/package.json | 8 ++++---- .../hwk-trezor-connector-webusb/package.json | 6 +++--- packages/hwk-trezor-connector/package.json | 12 ++++++------ packages/hwk-trezor-core/package.json | 8 ++++---- packages/hwk-trezor-protobuf/package.json | 4 ++-- packages/hwk-trezor-protocol/package.json | 2 +- packages/hwk-trezor-schema-utils/package.json | 4 ++-- packages/hwk-trezor-transport/package.json | 10 +++++----- packages/hwk-trezor-type-utils/package.json | 2 +- packages/hwk-trezor-utils/package.json | 4 ++-- packages/shared/package.json | 2 +- 34 files changed, 117 insertions(+), 117 deletions(-) diff --git a/packages/connect-examples/electron-example/package.json b/packages/connect-examples/electron-example/package.json index 33610788c..2fb46061d 100644 --- a/packages/connect-examples/electron-example/package.json +++ b/packages/connect-examples/electron-example/package.json @@ -2,7 +2,7 @@ "name": "hardware-example", "productName": "HardwareExample", "executableName": "onekey-hardware-example", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "description": "End-to-end encrypted workspaces for teams", "main": "dist/index.js", @@ -22,7 +22,7 @@ "ts:check": "yarn tsc --noEmit" }, "dependencies": { - "@onekeyfe/hd-transport-electron": "1.1.34-alpha.1", + "@onekeyfe/hd-transport-electron": "1.1.34-alpha.4", "@stoprocent/noble": "2.3.16", "debug": "4.3.4", "electron-is-dev": "^3.0.1", diff --git a/packages/connect-examples/expo-example/package.json b/packages/connect-examples/expo-example/package.json index bc492d2ff..c43aa292d 100644 --- a/packages/connect-examples/expo-example/package.json +++ b/packages/connect-examples/expo-example/package.json @@ -1,6 +1,6 @@ { "name": "expo-example", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "scripts": { "start": "cross-env CONNECT_SRC=https://localhost:8087/ yarn expo start --dev-client", "android": "yarn expo run:android", @@ -19,10 +19,10 @@ "@noble/ed25519": "^2.1.0", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^1.7.1", - "@onekeyfe/hd-ble-sdk": "1.1.34-alpha.1", - "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.1", - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-web-sdk": "1.1.34-alpha.1", + "@onekeyfe/hd-ble-sdk": "1.1.34-alpha.4", + "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.4", + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-web-sdk": "1.1.34-alpha.4", "@onekeyfe/react-native-ble-utils": "^0.1.3", "@polkadot/util-crypto": "13.1.1", "@react-native-async-storage/async-storage": "1.21.0", diff --git a/packages/connect-examples/expo-playground/package.json b/packages/connect-examples/expo-playground/package.json index 93d376344..7abb6627b 100644 --- a/packages/connect-examples/expo-playground/package.json +++ b/packages/connect-examples/expo-playground/package.json @@ -1,6 +1,6 @@ { "name": "onekey-hardware-playground", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "sideEffects": [ "app/utils/shim.js", @@ -17,9 +17,9 @@ }, "dependencies": { "@noble/hashes": "^1.8.0", - "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.1", - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", + "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.4", + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", diff --git a/packages/connect-examples/hwk-demo/package.json b/packages/connect-examples/hwk-demo/package.json index 8520fecae..5c38f34a0 100644 --- a/packages/connect-examples/hwk-demo/package.json +++ b/packages/connect-examples/hwk-demo/package.json @@ -1,6 +1,6 @@ { "name": "hwk-demo", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "main": "index.js", "scripts": { @@ -13,14 +13,14 @@ }, "dependencies": { "@expo/metro-runtime": "~3.1.3", - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.1", - "@onekeyfe/hwk-ledger-connector-ble": "1.1.34-alpha.1", - "@onekeyfe/hwk-ledger-connector-webhid": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector-rn-ble": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector-webusb": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.4", + "@onekeyfe/hwk-ledger-connector-ble": "1.1.34-alpha.4", + "@onekeyfe/hwk-ledger-connector-webhid": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector-rn-ble": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector-webusb": "1.1.34-alpha.4", "@react-native-async-storage/async-storage": "1.21.0", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", diff --git a/packages/core/package.json b/packages/core/package.json index b0bdc8054..75a4ba2de 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-core", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Core processes and APIs for communicating with OneKey hardware devices.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -25,8 +25,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "axios": "1.15.2", "bignumber.js": "^9.0.2", "bytebuffer": "^5.0.1", diff --git a/packages/hd-ble-sdk/package.json b/packages/hd-ble-sdk/package.json index aef0138e1..5bd4700d4 100644 --- a/packages/hd-ble-sdk/package.json +++ b/packages/hd-ble-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-ble-sdk", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,8 +20,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-react-native": "1.1.34-alpha.1" + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-react-native": "1.1.34-alpha.4" } } diff --git a/packages/hd-cli/package.json b/packages/hd-cli/package.json index 059ea1f68..e623a1cab 100644 --- a/packages/hd-cli/package.json +++ b/packages/hd-cli/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hardware-cli", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "OneKey hardware wallet CLI for testing device communication", "author": "OneKey", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "test": "jest" }, "dependencies": { - "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.1", - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-usb": "1.1.34-alpha.1", + "@onekeyfe/hd-common-connect-sdk": "1.1.34-alpha.4", + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-usb": "1.1.34-alpha.4", "commander": "^12.0.0" } } diff --git a/packages/hd-common-connect-sdk/package.json b/packages/hd-common-connect-sdk/package.json index 88ec42f16..f94e76dbf 100644 --- a/packages/hd-common-connect-sdk/package.json +++ b/packages/hd-common-connect-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-common-connect-sdk", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,12 +20,12 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-emulator": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-http": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-lowlevel": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-usb": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-web-device": "1.1.34-alpha.1" + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-emulator": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-http": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-lowlevel": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-usb": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-web-device": "1.1.34-alpha.4" } } diff --git a/packages/hd-transport-electron/package.json b/packages/hd-transport-electron/package.json index 6a1ce82b6..2eb74faae 100644 --- a/packages/hd-transport-electron/package.json +++ b/packages/hd-transport-electron/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-electron", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -25,9 +25,9 @@ "electron-log": ">=4.0.0" }, "dependencies": { - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "@stoprocent/noble": "2.3.16", "p-retry": "^4.6.2" }, diff --git a/packages/hd-transport-emulator/package.json b/packages/hd-transport-emulator/package.json index 4efdedc64..6592709b1 100644 --- a/packages/hd-transport-emulator/package.json +++ b/packages/hd-transport-emulator/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-emulator", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "hardware emulator transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-http/package.json b/packages/hd-transport-http/package.json index 1b577c4a5..e95e3196c 100644 --- a/packages/hd-transport-http/package.json +++ b/packages/hd-transport-http/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-http", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "hardware http transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-lowlevel/package.json b/packages/hd-transport-lowlevel/package.json index 0c524a8de..9820bdb5c 100644 --- a/packages/hd-transport-lowlevel/package.json +++ b/packages/hd-transport-lowlevel/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-lowlevel", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -19,7 +19,7 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1" + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4" } } diff --git a/packages/hd-transport-react-native/package.json b/packages/hd-transport-react-native/package.json index 095d904fe..493b1d31f 100644 --- a/packages/hd-transport-react-native/package.json +++ b/packages/hd-transport-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-react-native", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -19,9 +19,9 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "@onekeyfe/react-native-ble-utils": "^0.1.6", "react-native-ble-plx": "3.5.1" } diff --git a/packages/hd-transport-usb/package.json b/packages/hd-transport-usb/package.json index ae7eb164f..730cdc472 100644 --- a/packages/hd-transport-usb/package.json +++ b/packages/hd-transport-usb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-usb", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "OneKey hardware wallet direct USB transport plugin (libusb)", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -20,8 +20,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4", "bytebuffer": "^5.0.1", "usb": "^2.14.0" } diff --git a/packages/hd-transport-web-device/package.json b/packages/hd-transport-web-device/package.json index 8000cf800..458f0c9bd 100644 --- a/packages/hd-transport-web-device/package.json +++ b/packages/hd-transport-web-device/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-web-device", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -20,11 +20,11 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport": "1.1.34-alpha.1" + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport": "1.1.34-alpha.4" }, "devDependencies": { - "@onekeyfe/hd-transport-electron": "1.1.34-alpha.1", + "@onekeyfe/hd-transport-electron": "1.1.34-alpha.4", "@types/w3c-web-usb": "^1.0.6", "@types/web-bluetooth": "^0.0.17" } diff --git a/packages/hd-transport/package.json b/packages/hd-transport/package.json index e27ed52ba..832236062 100644 --- a/packages/hd-transport/package.json +++ b/packages/hd-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Transport layer abstractions and utilities for OneKey hardware SDK.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", diff --git a/packages/hd-web-sdk/package.json b/packages/hd-web-sdk/package.json index 17cfa8cb3..71903a640 100644 --- a/packages/hd-web-sdk/package.json +++ b/packages/hd-web-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-web-sdk", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -23,10 +23,10 @@ }, "dependencies": { "@onekeyfe/cross-inpage-provider-core": "2.2.67", - "@onekeyfe/hd-core": "1.1.34-alpha.1", - "@onekeyfe/hd-shared": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-http": "1.1.34-alpha.1", - "@onekeyfe/hd-transport-web-device": "1.1.34-alpha.1" + "@onekeyfe/hd-core": "1.1.34-alpha.4", + "@onekeyfe/hd-shared": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-http": "1.1.34-alpha.4", + "@onekeyfe/hd-transport-web-device": "1.1.34-alpha.4" }, "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.17.12", diff --git a/packages/hwk-adapter-core/package.json b/packages/hwk-adapter-core/package.json index 4e07c5237..5f7238b74 100644 --- a/packages/hwk-adapter-core/package.json +++ b/packages/hwk-adapter-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-adapter-core", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Shared types and utilities for OneKey hardware wallet kit", "author": "OneKey", "license": "MIT", diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index a095af3e5..597f3066b 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-adapter", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Ledger hardware wallet adapter for OneKey", "author": "OneKey", "license": "MIT", @@ -51,7 +51,7 @@ "@ledgerhq/hw-app-trx": "^6.34.1", "@ledgerhq/hw-transport": "^6.34.1", "@noble/hashes": "^1.3.3", - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" }, "devDependencies": { diff --git a/packages/hwk-ledger-connector-ble/package.json b/packages/hwk-ledger-connector-ble/package.json index 2fa41d849..105c9053f 100644 --- a/packages/hwk-ledger-connector-ble/package.json +++ b/packages/hwk-ledger-connector-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-ble", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "IConnector implementation for Ledger hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -51,8 +51,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-react-native-ble": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.1" + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.4" }, "peerDependencies": { "react-native": "*" diff --git a/packages/hwk-ledger-connector-webhid/package.json b/packages/hwk-ledger-connector-webhid/package.json index a719afe78..41392f1d7 100644 --- a/packages/hwk-ledger-connector-webhid/package.json +++ b/packages/hwk-ledger-connector-webhid/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-webhid", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "IConnector implementation for Ledger hardware wallets via WebHID (DMK)", "author": "OneKey", "license": "MIT", @@ -49,8 +49,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-web-hid": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.1" + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-ledger-adapter": "1.1.34-alpha.4" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index 14381c373..ff71b0d39 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-adapter", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Trezor hardware wallet adapter primitives for OneKey", "author": "OneKey", "license": "MIT", @@ -45,8 +45,8 @@ "dependencies": { "@noble/curves": "^1.9.7", "@noble/hashes": "^1.3.3", - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.4", "tslib": "^2.6.2" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector-electron-ble/package.json b/packages/hwk-trezor-connector-electron-ble/package.json index 45e9bb8a8..2e106e5d9 100644 --- a/packages/hwk-trezor-connector-electron-ble/package.json +++ b/packages/hwk-trezor-connector-electron-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-electron-ble", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "IConnector implementation for Trezor hardware wallets via Electron BLE (noble in main process)", "author": "OneKey", "license": "MIT", @@ -66,9 +66,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.4", "buffer": "^6.0.3" }, "peerDependencies": { diff --git a/packages/hwk-trezor-connector-rn-ble/package.json b/packages/hwk-trezor-connector-rn-ble/package.json index b9b520b7a..8ac7778c1 100644 --- a/packages/hwk-trezor-connector-rn-ble/package.json +++ b/packages/hwk-trezor-connector-rn-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-rn-ble", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "IConnector implementation for Trezor hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -46,9 +46,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-adapter": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.4", "buffer": "^6.0.3", "react-native-ble-plx": "^3.5.1" }, diff --git a/packages/hwk-trezor-connector-webusb/package.json b/packages/hwk-trezor-connector-webusb/package.json index bf8091765..33e7b937b 100644 --- a/packages/hwk-trezor-connector-webusb/package.json +++ b/packages/hwk-trezor-connector-webusb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-webusb", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "IConnector implementation for Trezor hardware wallets via WebUSB", "author": "OneKey", "license": "MIT", @@ -55,8 +55,8 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.1", + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-connector": "1.1.34-alpha.4", "buffer": "^6.0.3" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector/package.json b/packages/hwk-trezor-connector/package.json index 70e599f8f..9c1fa699a 100644 --- a/packages/hwk-trezor-connector/package.json +++ b/packages/hwk-trezor-connector/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Shared Trezor connector base for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -38,13 +38,13 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.1" + "@onekeyfe/hwk-adapter-core": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-core": "1.1.34-alpha.4" }, "devDependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-transport": "1.1.34-alpha.1", + "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-transport": "1.1.34-alpha.4", "rimraf": "^5.0.0", "tsup": "^8.0.0", "typescript": "5.1.6" diff --git a/packages/hwk-trezor-core/package.json b/packages/hwk-trezor-core/package.json index c456932e3..71ee8bdb6 100644 --- a/packages/hwk-trezor-core/package.json +++ b/packages/hwk-trezor-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-core", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Minimal Trezor core call layer for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -41,9 +41,9 @@ "devDependencies": { "@noble/ciphers": "^1.3.0", "@noble/hashes": "^1.8.0", - "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-transport": "1.1.34-alpha.1", + "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-transport": "1.1.34-alpha.4", "buffer": "^6.0.3", "rimraf": "^5.0.0", "tsup": "^8.0.0", diff --git a/packages/hwk-trezor-protobuf/package.json b/packages/hwk-trezor-protobuf/package.json index 2ce566e9c..cc49031d2 100644 --- a/packages/hwk-trezor-protobuf/package.json +++ b/packages/hwk-trezor-protobuf/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protobuf", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Trezor protobuf helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -50,7 +50,7 @@ "sideEffects": false, "dependencies": { "@bufbuild/protobuf": "^2.11.0", - "@onekeyfe/hwk-trezor-schema-utils": "1.1.34-alpha.1" + "@onekeyfe/hwk-trezor-schema-utils": "1.1.34-alpha.4" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-protocol/package.json b/packages/hwk-trezor-protocol/package.json index 26c2dba49..c330bd9f6 100644 --- a/packages/hwk-trezor-protocol/package.json +++ b/packages/hwk-trezor-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protocol", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Trezor transport protocol helpers vendored from Trezor Suite MIT sources", "author": "OneKey", diff --git a/packages/hwk-trezor-schema-utils/package.json b/packages/hwk-trezor-schema-utils/package.json index 7e7cc5a31..307489948 100644 --- a/packages/hwk-trezor-schema-utils/package.json +++ b/packages/hwk-trezor-schema-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-schema-utils", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Schema helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -44,7 +44,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.1", + "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.4", "@sinclair/typebox": "^0.34.49", "ts-mixer": "^6.0.4" }, diff --git a/packages/hwk-trezor-transport/package.json b/packages/hwk-trezor-transport/package.json index 985bc5ebb..620260105 100644 --- a/packages/hwk-trezor-transport/package.json +++ b/packages/hwk-trezor-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-transport", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Low-level Trezor transport helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -49,10 +49,10 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.1", - "@onekeyfe/hwk-trezor-utils": "1.1.34-alpha.1", + "@onekeyfe/hwk-trezor-protobuf": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-protocol": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.4", + "@onekeyfe/hwk-trezor-utils": "1.1.34-alpha.4", "cross-fetch": "^4.1.0", "usb": "^2.17.0" }, diff --git a/packages/hwk-trezor-type-utils/package.json b/packages/hwk-trezor-type-utils/package.json index 930da72c2..7990215ce 100644 --- a/packages/hwk-trezor-type-utils/package.json +++ b/packages/hwk-trezor-type-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-type-utils", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Type helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", diff --git a/packages/hwk-trezor-utils/package.json b/packages/hwk-trezor-utils/package.json index 220c1fc77..ba9cf915c 100644 --- a/packages/hwk-trezor-utils/package.json +++ b/packages/hwk-trezor-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-utils", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "private": true, "description": "Utility helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -39,7 +39,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.1", + "@onekeyfe/hwk-trezor-type-utils": "1.1.34-alpha.4", "bignumber.js": "^9.3.1", "events": "^3.3.0" }, diff --git a/packages/shared/package.json b/packages/shared/package.json index f1bf7be62..39633f345 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-shared", - "version": "1.1.34-alpha.1", + "version": "1.1.34-alpha.4", "description": "Hardware SDK's shared tool library", "keywords": [ "Hardware-SDK",