QR 서명
페어링 후에는 모든 서명이 한 번의 왕복으로 끝납니다. 지갑이 요청 QR 코드를 표시하면, BitTool Signer가 응답 QR 코드를 표시합니다.
| 체인 | 요청(지갑 → 서명 기기) | 응답(서명 기기 → 지갑) | 페이지 |
|---|---|---|---|
| Bitcoin | ur:crypto-psbt (ur:psbt, BBQr, Base64/hex 텍스트도 지원) | 같은 타입에 서명을 추가한 것 | 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 맵이며, UR 본문으로 바로 전달됩니다(지갑 레벨 페어링과 달리 CBOR 계층이 하나입니다). 세 가지 구성 요소를 공통으로 사용합니다.
요청 ID: 37(bytes)
CBOR 태그 37로 바이트 문자열을 감싼 무작위 식별자입니다. BitTool Signer는 이 값을 응답에 그대로 복사하므로, 응답을 대기 중인 요청과 짝지을 수 있습니다.
- 요청마다 무작위 16바이트를 생성하세요(UUID의 텍스트가 아니라 바이트).
sol-sign-request와tron-sign-request는 정확히 16바이트여야 합니다.eth-sign-request는 더 긴 불투명 ID를 쓰는 지갑과의 호환을 위해 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입니다. 알 수 없는 키가 하나라도 있으면 요청 전체가 무효가 되므로, 자체 메타데이터를 추가하지 마세요.
애니메이션 QR 코드
요청은 QR 코드 하나에 담기지 않는 경우가 많습니다. 멀티파트 UR로 인코딩하고 프레임을 반복 재생하세요.
- UR 텍스트를 대문자로 바꾸세요.
UR:ETH-SIGN-REQUEST/…는 QR 영숫자(alphanumeric) 모드에 맞으며, 바이트 모드보다 약 30% 적은 비트가 필요합니다. - 프래그먼트당 200–400바이트, 초당 3–5프레임이면 휴대폰에서 안정적으로 스캔됩니다.
- 첫 번째 회차를 다시 재생하지 말고 인코더의
nextPart()를 계속 호출하세요. 첫 회차가 끝나면 bc-ur 인코더는 여러 조각을 섞은 "파운틴(fountain)" 파트를 생성하며, 덕분에 스캐너는 프레임을 놓쳐도 스캔을 완료할 수 있습니다.
BitTool Signer는 자체 응답을 180바이트 프래그먼트로 인코딩합니다. 따라서 짧은 서명은 정적 QR 코드 하나로, 서명된 PSBT는 애니메이션으로 표시될 수 있습니다. 스캐너는 두 경우를 모두 처리해야 합니다.
응답
- 먼저 UR 타입을 확인하세요.
eth-sign-request에 대한 응답은eth-signature여야 하는 식입니다. Bitcoin은 보낸 것과 같은 타입으로 응답합니다. - 요청 ID를 확인하세요(Bitcoin: PSBT 안의 서명되지 않은 트랜잭션이 바뀌지 않았는지 확인).
- 예상한 키와 서명을 요청한 정확한 바이트에 대해 서명을 암호학적으로 검증한 다음, 조립해 브로드캐스트하세요.
거부 응답은 없습니다. 사용자가 BitTool Signer에서 취소하면 아무것도 표시되지 않습니다. UI에서 사용자가 이전 단계로 돌아가거나, QR을 다시 표시하거나, 처음부터 다시 시작할 수 있게 하고, 응답을 무한정 기다리지 않도록 하세요.
대기 레코드 관리
각 요청은 응답을 받거나 폐기될 때까지 저장하세요.
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는 요청에 담긴 경로, 지문, 주소 또는 공개 키를 iPhone에 있는 모든 지갑의 공개 키와 비교해 스스로 판단합니다. 정확히 하나의 지갑만 일치해야 합니다. 전체 규칙은 서명 기기의 확인 항목을 참고하세요. 실무적으로는 모든 요청에 전체 경로, 지문, 주소(또는 공개 키)를 넣으면 BitTool Signer가 키를 바로 찾습니다.