通过二维码签名
配对完成后,每次签名都是一次往返:你的钱包显示请求二维码,BitTool Signer 显示响应二维码。
| 链 | 请求(你 → 签名器) | 响应(签名器 → 你) | 页面 |
|---|---|---|---|
| Bitcoin | ur:crypto-psbt(也支持 ur:psbt、BBQr、Base64/十六进制文本) | 相同类型,附加了签名 | Bitcoin |
| EVM | ur:eth-sign-request | ur:eth-signature | EVM |
| TRON | ur:tron-sign-request | ur:tron-signature | TRON |
| Solana | ur:sol-sign-request | ur:sol-signature | Solana |
这些都是公开的 Keystone UR 注册表类型。钱包级配对格式仅用于导入账户;不存在 BitTool 专有的签名封装格式,也不应自行发明。
请求的组成部分
EVM、TRON 和 Solana 的请求都是使用整数键的 CBOR map,直接作为 UR 正文传输(只有一层 CBOR,与钱包级配对不同)。它们共用三个组成部分。
请求 ID:37(bytes)
一个随机标识符,即用 CBOR 标签 37 包裹的字节串。BitTool Signer 会将其原样复制到响应中,方便你把响应与待处理的请求对应起来。
- 为每个请求生成 16 个随机字节(UUID 的字节,而不是其文本形式)。
sol-sign-request和tron-sign-request要求恰好 16 字节。为兼容使用更长不透明 ID 的钱包,eth-sign-request也接受 1–64 字节。- 按字节比较 ID。ID 匹配并不代表签名有效;你仍然需要验证签名。
派生路径:304(crypto-keypath)
需要签名的密钥的完整路径,以及主指纹:
304({
1: [44, true, 60, true, 0, true, 0, false, 0, false], ; m/44'/60'/0'/0/0: index, hardened, index, hardened …
2: 1942346250 ; master fingerprint 0x73c5da0a as a big-endian uint32
})
- 路径分量按
index, hardened?交替排列。不要再给硬化索引加上0x80000000。 - 使用具体的叶子路径(
…/0/5),不要使用账户路径或*通配符。 - 始终包含指纹(键 2)。缺少指纹时匹配能力会减弱,部分请求甚至完全无法匹配。
一个使用 cborg 构建该结构的辅助函数:
import { Tagged } from 'cborg';
/** crypto-keypath (tag 304): "m/44'/195'/0'/0/0" + "73c5da0a" →
* 304({1: [44, true, 195, true, 0, true, 0, false, 0, false], 2: 1942346250}) */
export function keypath(path, masterFingerprint) {
const components = path.replace(/^m\//, '').split('/')
.flatMap((part) => [Number.parseInt(part, 10), part.endsWith("'")]);
return new Tagged(304, new Map([[1, components], [2, Number.parseInt(masterFingerprint, 16)]]));
}
Origin:文本
向用户显示的简短标签,表示请求的来源,例如 "BitTool Wallet"。最多 256 个字符。它仅用于显示,不经过认证:BitTool Signer 从不把它当作任何证明。
不允许额外字段
只接受各类型定义的键:eth-sign-request 为 1–7,sol- 和 tron-sign-request 为 1–6。任何未知的键都会使整个请求无效,因此不要添加你自己的元数据。
动态二维码
请求经常超出单个二维码的容量。请将其编码为多部分 UR,并循环播放各帧:
- 将 UR 文本转为大写:
UR:ETH-SIGN-REQUEST/…可以使用二维码的字母数字模式,所需比特数比字节模式少约 30%。 - 每个分片 200–400 字节、每秒 3–5 帧,在手机上扫描较为可靠。
- 持续调用编码器的
nextPart(),而不是重复播放第一轮。第一轮之后,bc-ur 编码器会生成混合的“喷泉码”分片,即使漏扫了部分帧,扫描器也能完成解码。
BitTool Signer 使用 180 字节的分片编码自己的响应,因此较短的签名是单个静态二维码,而已签名的 PSBT 可能是动态二维码。你的扫描器必须能处理这两种情况。
响应
- 先检查 UR 类型:
eth-sign-request的响应必须是eth-signature,依此类推。Bitcoin 以你发送时所用的相同类型返回。 - 检查请求 ID(Bitcoin:检查 PSBT 内的未签名交易没有被改动)。
- 使用预期的密钥、针对你请求签名的确切字节,对签名进行密码学验证,然后再组装并广播。
不存在拒绝响应。如果用户在 BitTool Signer 中取消,不会显示任何内容。你的界面应允许用户返回、重新显示二维码或重新开始,并且绝不能无限期地等待回复。
保存待处理记录
保存每个请求,直到它得到响应或被放弃:
pendingId, walletId, chain/network, signer path + key/address,
requestId, request type, exact bytes to sign, digest,
unsigned transaction, created at, expiry (nonce / blockhash / TRON expiration),
status: awaitingSignature → signatureVerified → broadcast → confirmed | failed | cancelled
如果交易中有任何内容发生变化(手续费、nonce、blockhash、金额),请创建一个使用新 ID 的新请求。绝不要把一个签名用于不同的字节;对于已取消或已过期的请求,收到的响应应予以拒绝。
BitTool Signer 如何找到密钥
你无需告诉 BitTool Signer 使用哪个钱包;它会把请求中的路径、指纹以及地址或公钥,与手机上每个钱包的公钥进行比对,自行确定。必须恰好有一个钱包匹配。完整规则参见签名器会检查什么。实践中:在每个请求中都发送完整路径、指纹和地址(或公钥),BitTool Signer 就能直接找到密钥。