└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Universal Offline Signatures 2 | 3 | **THIS IS A DRAFT** 4 | 5 | This document proposes a standard for QR code encoding that enables two-way communication between a _Hot Wallet_ and a _Cold Signer_ with access to private keys, for the purpose of securely signing and broadcasting transactions and data on current and future decentralized networks (including non-Ethereum networks). The goal is have a single, inter-operable standard, allowing users to use any combination of a Hot Wallet and a Cold Signer (defined below) without vendor locking. 6 | 7 | ## Design principles 8 | 9 | + **Concise** - a single QR code can represent up to 23624 (±4) bits of information. The more data that must be represented, the denser the code becomes, making it problematic to use with cheaper hardware, therefore packing as little data as possible per QR code is the pragmatic thing to do. 10 | + **Unambiguous** - there must be one, and only one, way for the payload to be signed to be interpreted by a correct implementation following this standard. 11 | + **Extensible** - it should be possible to add support for new networks and new cryptography on existing networks (should such need emerge) in the future, without breaking backwards compatibility. 12 | 13 | ## [QR code encoding](https://en.wikipedia.org/wiki/QR_code#Storage) 14 | 15 | The common ways to encode binary data in a QR code would include: 16 | 17 | + Base64 US-ASCII representation with Binary QR encoding: ~33.3% overhead. 18 | + Hexadecimal representation with Alphanumeric QR encoding: 37.5% overhead. 19 | + Hexadecimal US-ASCII representation with Binary QR encoding: 100% overhead. 20 | + Native Binary QR encoding: *no overhead*. 21 | 22 | For data density and simplicity **this standard will only use the native Binary QR encoding**. 23 | 24 | _Note:_ Base64 US-ASCII representation with Alphanumeric QR encoding is impossible, as Alphanumeric QR code only permits 44 (5½ bits per character) out of the required 64 characters (6 bits per character). 25 | 26 | ## Nomenclature 27 | 28 | Since this technology requires two separate devices/applications, to avoid confusion the following names will be used to differentiate the two: 29 | 30 | + **Hot Wallet** - an application running on an Internet connected device that has the ability to show and scan QR codes, produce and encode transactions or data to be signed, and broadcast them to appropriate network. 31 | + **Cold Signer** - a device or an application running on a dedicated device without Internet access that has the ability to show and scan QR codes, securely store private keys, decode transactions or data to be signed, and sign them. 32 | 33 | For describing binary data this standard uses either a single byte index `[n]`, an open left-inclusive range `[n..]`, or a closed left-incluse right-exclusive range `[n..m]`. `[..n]` is a shorthand for `[0..n]`. Examples: 34 | 35 | + `[3]` is a single byte at index `3`. 36 | + `[0..5]` is 5 bytes at following indexes: `0`, `1`, `2`, `3`, and `4`. 37 | + `[..5]` is also 5 bytes at following indexes: `0`, `1`, `2`, `3`, and `4`. 38 | + `[7..7]` would be a zero-length range and contain no bytes. 39 | + `[10..]` would be all bytes starting from index `10` till the end. 40 | 41 | For byte values this standard uses either a single hexadecimal value `AA`, or a range `AA...BB`, which is left and right inclusive: 42 | 43 | + `00` is a single US-ASCII `nul` byte. 44 | + `61...7A` is a range including all lowercase US-ASCII letters `a` to `z`. 45 | 46 | Additionally we will define the following terms to mean: 47 | 48 | + **MUST** and **MUST NOT** - expected behavior, breaking which break compatibility with this standard. 49 | + **SHOULD** and **SHOULD NOT** - expected behavior although more fuzzily defined and breaking of which does not break compatibility with this standard. 50 | + **MAY** - behavior that is not part of this standard, but is allowed without breaking compatibility. 51 | 52 | ## Steps 53 | 54 | Since this is a multi-step process, we will differentiate between the following types of QR codes: 55 | 56 | | Step | Name | Direction | Contains | QR Encoding | 57 | |------|----------------------------------------|------------|-------------------------------------|----------------| 58 | | 0⁽¹⁾ | [**Introduction**](#introduction-step) | Cold ⇒ Hot | Network identification and Address | Binary (UTF-8) | 59 | | 1 | [**Payload**](#payload-step) | Cold ⇐ Hot | Data to sign prefixed with metadata | Binary | 60 | | 2 | [**Signature**](#isgnature-step) | Cold ⇒ Hot | Signature for **Payload** | Binary | 61 | 62 | + ⁽¹⁾ Step 0 is optional as it is only necessary if the Hot Wallet doesn't yet know the address which it must use in Step 1. 63 | 64 | --- 65 | 66 | ### *Introduction* Step 67 | 68 | The goal of this step is for Cold Signer to inform the Hot Wallet about a single account it has access to. To make this useful outside of the scope of this specification, this standard proposes using URI format compatible with [EIP-681](https://eips.ethereum.org/EIPS/eip-681) and [EIP-831](https://eips.ethereum.org/EIPS/eip-831), with syntax: 69 | 70 | ``` 71 | introduction = scheme ":" details 72 | scheme = STRING 73 | details = STRING 74 | ``` 75 | 76 | + The `details` format depends on the `scheme`. 77 | + `scheme` **MUST** be valid US-ASCII, beginning with a letter and followed by any number of letters, numbers, the period `.` character, the plus `+` character, or the hyphen `-` character. 78 | + `details` **MUST** be valid UTF-8, appropriate for a given network. 79 | + Cold Signer **MUST NOT** add any other information other than `scheme` and `details` to the string. 80 | + Hot Wallet **MAY** be able to read other information than required (such as is defined in EIP-681). 81 | + Hot Wallet **MAY** support any number of schemes/networks following this syntax. 82 | + For unsupported schemes/networks Hot Wallet **MUST** show the user an informative error, distinct from parsing failure, eg: `"Scheme {scheme} is not supported by {wallet name}"`. 83 | 84 | #### Ethereum Introduction 85 | 86 | ``` 87 | details = address | address "@" chainid | address "@" chainid ":" name 88 | ``` 89 | 90 | + `scheme` **MUST** be a string `ethereum`. 91 | + `address` **MUST** be a hexadecimal string representation of the address. 92 | + `address` **MUST** be prefixed with `0x` 93 | + `chainid` **MUST** be a decimal number. 94 | + `chainid` **SHOULD** map onto a proper value at [https://chainid.network/](https://chainid.network/). 95 | + `name` is an optional display name. 96 | 97 | A correct Introduction for address zero (`0x0000000000000000000000000000000000000000`) on Ethereum is therefore a string: 98 | 99 | ``` 100 | ethereum:0x0000000000000000000000000000000000000000@1 101 | ``` 102 | 103 | #### Substrate Introduction 104 | 105 | ``` 106 | details = address | address ":" genesishash | address ":" genesishash ":" name 107 | ``` 108 | 109 | + `scheme` **MUST** be a string `substrate`. 110 | + `address` **MUST** be base58 representation of the address. 111 | + `genesishash` **MUST** be a hexadecimal representation of the genesis hash of a given substrate network. 112 | + `genesishash` **MUST** be prefixed with `0x`. 113 | + `name` **MUST** be valid UTF-8 and can include the character `:`. 114 | 115 | A correct Introduction for address `5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX` on a Substrate-based network is therefore a string: 116 | 117 | ``` 118 | substrate:5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX 119 | ``` 120 | 121 | --- 122 | 123 | ### *Payload* Step. 124 | 125 | Payload is always read left-to-right, using prefixing to determine how it needs to be read. The first prefix is single byte at index `0`: 126 | 127 | | `[0]` | `[1..]` | 128 | |-----------|-----------------------------------------------------| 129 | | `00` | [**Multipart Payload**](#multipart-payload) | 130 | | `01...44` | Extension range for other networks | 131 | | `45` | [Ethereum Payload](#ethereum-payload) | 132 | | `46...52` | Extension range for other networks | 133 | | `53` | [Substrate Payload](#substrate-payload) | 134 | | `54...7A` | Extension range for other networks | 135 | | `7B` | [Legacy Ethereum Payload](#legacy-ethereum-payload) | 136 | | `7C...7F` | Extension range for other networks | 137 | | `80...FF` | Reserved | 138 | 139 | #### *Multipart Payload* 140 | 141 | QR codes can only represent 2953 bytes, which is a harsh constraint as some transactions, such as contract deployment, may not fit into a single code. Multipart Payload is a way to represent a single Payload as a series of QR codes. Each QR code in Multipart Payload, or _a frame_, looks as follows: 142 | 143 | | `[0]` | `[1..3]` | `[3..5]` | `[5..]` | 144 | |--------|----------|---------------|-------------| 145 | | `00` | `frame` | `frame_count` | `part_data` | 146 | 147 | + `frame` **MUST** the number of current frame, represented as big-endian 16-bit unsigned integer. 148 | + `frame_count` **MUST** the total number of frames, represented as big-endian 16-bit unsigned integer. 149 | + `part_data` **MUST** be stored by the Cold Signer, ordered by `frame` number, until all frames are scanned. 150 | + Hot Wallet **MUST** continuously loop through all the frames showing each frame for about 2 seconds. 151 | + Cold Signer **MUST** be able to start scanning the Multipart Payload _at any frame_. 152 | + Cold Signer **MUST NOT** expect the frames to come in any particular order. 153 | + Cold Signer **SHOULD** show a progress indicator of how many frames it has successfully scanned out of the total count. 154 | + `part_data` for frame `0` **MUST NOT** begin with byte `00` or byte `7B`. 155 | 156 | Once all frames are combined, the `part_data` must be concatenated into a single binary blob, and then interpreted as a completely new albeit larger Payload, starting from the prefix table above. 157 | 158 | #### Ethereum Payload 159 | 160 | Byte `45` is the US-ASCII byte representing the capital letter `E`. Ethereum Payload follows the table: 161 | 162 | | Action | `[0]` | `[1]` | `[2..22]` | `[22..]` | 163 | |--------------------|-------|-------|-----------|-----------| 164 | | Sign a hash | `45` | `00` | `address` | `hash` | 165 | | Sign a transaction | `45` | `01` | `address` | `rlp` | 166 | | Sign a message | `45` | `02` | `address` | `message` | 167 | 168 | + `address` **MUST NOT** have any prefixes. 169 | + `address` **MUST** be exactly 20 bytes long. 170 | + `address` **MUST** be represented as a binary byte string, **NOT** hexadecimal. 171 | + `rlp` **MUST** be the [RLP](https://github.com/ethereum/wiki/wiki/RLP) encoded raw transaction with an empty signature being set in accordance with [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md): `v = CHAIN_ID`, `r = 0`, `s = 0`. 172 | + `message` **MUST** be a binary or UTF-8 encoded message to sign **WITHOUT** any prefixes ([EIP-191](https://eips.ethereum.org/EIPS/eip-191) or otherwise). 173 | + `hash` **MUST** be a valid `keccak256` hash of either a transaction or a correctly prefixed message. 174 | + Hot Wallet **SHOULD** always prefer sending either a full raw transaction or full message instead of a hash to sign, so that the user can verify that the the Cold Signer is signing what the Hot Wallet presented them with. Occasionally this might be completely impractical (the message or the transaction is megabytes long and not suitable for Multipart Payload). 175 | + Cold Signer **SHOULD** decode the transaction details from the RLP and display them to the user, so that they can verify that the transaction hasn't been altered by the Hot Wallet. 176 | + Cold Signer **SHOULD** attempt to decode the `message` as UTF-8 encoded human readable string by whatever heuristics it finds suitable and display it to the user, so that the user can verify that the message hasn't been altered by the Hot Wallet. 177 | + Cold Signer **SHOULD** warn the user that signing a hash is inherently insecure, because there is no easy way for the user to verify whether they are signing what they intended to sign. 178 | + Hot Wallet **SHOULD** have a way to show [Legacy Ethereum Payload](#legacy-ethereum-payload) at user request. 179 | 180 | TODO: Handle [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data. 181 | 182 | #### Substrate Payload 183 | 184 | Byte `53` is the US-ASCII byte representing the capital letter `S`. Substrate Payload follows the table: 185 | 186 | | Action | `[0]` | `[1]` | `[2]` | `[1..1+L]` | `[1+L..]` | 187 | |------------------------------|-------|--------|--------|-------------|----------------------------| 188 | | Sign a transaction | `53` |`crypto`| `00` | `accountid` | `payload` | 189 | | Sign a transaction | `53` |`crypto`| `01` | `accountid` | `payload_hash` | 190 | | Sign an immortal transaction | `53` |`crypto`| `02` | `accountid` | `immortal_payload` | 191 | | Sign a message | `53` |`crypto`| `03` | `accountid` | `message` | 192 | 193 | 194 | + `crypto` **MUST** be a recognised cryptographic algorithm. It implies the value of the `accountid` length, `L`. This **MUST** be one byte whose value is one of: 195 | - `0x00`: Ed25519 (`L = 32`) 196 | - `0x01`: Schnorr/Ristretto x25519 (`L = 32`) 197 | + `accountid` **MUST** be exactly `L` bytes long. 198 | + `accountid` **MUST** be represented as a binary byte string, **NOT** hexadecimal. 199 | + `payload` **MUST** be the SCALE encoding of the tuple of transaction items `(nonce, call, era_description, era_header)`. 200 | + `payload_hash` **MUST** be the Blake2s 32-byte hash of the SCALE encoding of the tuple of transaction items `(nonce, call, era_description, era_header)`. 201 | + `immortal_payload` **MUST** be the SCALE encoding of the tuple of transaction items `(nonce, call)`. 202 | + Hot Wallet **MUST** use type `00` for signing a standard transaction type if the length of the `payload` is 256 bytes or fewer. 203 | + Hot Wallet **SHOULD** always prefer using type `00` even if the length of the payload is greater than 256 bytes since this allows the full payload to be provided and decoded for the user. If doing that is completely impractical (the message or the transaction is megabytes long and not suitable for Multipart Payload), type `01` may be used alternatively. 204 | + Cold Signer **SHOULD** decode the transaction details from the SCALE encoding and display them to the user for verification before signing. 205 | + Cold Signer **SHOULD** attempt to decode the `message` as UTF-8 encoded human readable string by whatever heuristics it finds suitable and display it to the user for verification before signing. 206 | + Cold Signer **SHOULD** warn the user that signing a hash is inherently insecure, in the cash of type `01`. 207 | + Cold Signer **SHOULD** (at the user's discretion) sign the `message`, `immortal_payload`, or `payload` if `payload` is of length 256 bytes or fewer. If `payload` is longer than 256 bytes, then it **SHOULD** instead sign the Blake2s hash of `payload`. 208 | + Cold Signer **SHOULD** display all account id values in SS58Check encoding. 209 | 210 | #### Legacy Ethereum Payload 211 | 212 | Byte `7B` is the US-ASCII byte representing open curly brace `{`, for that reason it's treated as a prefix for older, deprecated format. This Payload should be decoded in full as UTF-8 encoded JSON, following either of the two variants: 213 | 214 | ```json 215 | { 216 | "action": "signTransaction", 217 | "data": { 218 | "account": ADDRESS, 219 | "rlp": RLP 220 | } 221 | } 222 | ``` 223 | 224 | or 225 | 226 | ```json 227 | { 228 | "action": "signData", 229 | "data":{ 230 | "account": ADDRESS, 231 | "data": MESSAGE 232 | } 233 | } 234 | ``` 235 | 236 | + `ADDRESS` **MUST** be a hexadecimal string representation of the address, exactly 40 characters long. 237 | + `ADDRESS` **MUST NOT** include the `0x` prefix. 238 | + `RLP` **MUST** be a hexadecimal string representation of the [RLP](https://github.com/ethereum/wiki/wiki/RLP) encoded raw transaction with an empty signature being set in accordance with [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md): `v = CHAIN_ID`, `r = 0`, `s = 0`. 239 | + `RLP` **MUST NOT** include the `0x` prefix. 240 | + `DATA` **MUST** be a hexadecimal string representation of a binary or UTF-8 encoded message to sign **WITHOUT** any prefixes ([EIP-191](https://eips.ethereum.org/EIPS/eip-191) or otherwise). 241 | + `DATA` **MUST NOT** include the `0x` prefix. 242 | + All _**SHOULD**s_ from [Ethereum Payload](#ethereum-payload) apply here as well. 243 | + Legacy Ethereum Payload does not support signing raw hashes. 244 | 245 | --- 246 | 247 | ### *Signature* Step 248 | 249 | Signatures will vary on type of payload that is being signed. 250 | 251 | #### Ethereum Signature 252 | 253 | Ethereum signature must follow one of the two following formats: 254 | 255 | | `[0]` | `[1..33]` | `[33..65]` | `[66]` | 256 | |-------|-----------|------------|--------| 257 | | `01` | `r` | `s` | `v` | 258 | 259 | or 260 | 261 | | `[0..64]` | `[64..128]` | `[128..130]` | 262 | |-----------|-------------|--------------| 263 | | `HEX_R` | `HEX_S` | `HEX_V` | 264 | 265 | + Cold Signer **SHOULD** prefer the first format as it's more concise. 266 | + Hot Wallet **MUST** first check byte length and assume second format if length equals `130`. 267 | + Hot Wallet **MUST** support both formats. 268 | + `r` **MUST** be binary `r` value of the Secp256k1 signature for the signed Payload. 269 | + `s` **MUST** be binary `s` value of the Secp256k1 signature for the signed Payload. 270 | + `v` **MUST** be binary `v` value of the Secp256k1 signature for the signed Payload. 271 | + `HEX_R` **MUST** be a hexadecimal representation of `r` value of the Secp256k1 signature for the signed Payload. 272 | + `HEX_S` **MUST** be a hexadecimal representation of `s` value of the Secp256k1 signature for the signed Payload. 273 | + `HEX_V` **MUST** be a hexadecimal representation of `b` value of the Secp256k1 signature for the signed Payload. 274 | + `HEX_R`, `HEX_S`, and `HEX_V` **MUST NOT** be prefixed with `0x`. 275 | + `v` and `HEX_V` **MUST NOT** be combined with `CHAIN_ID`. 276 | + Hot Wallet **MUST** fold `CHAIN_ID` into the `v` value when constructing final transaction RLP. 277 | 278 | Pseudocode for folding in `CHAIN_ID` into `v`: 279 | 280 | ``` 281 | if chainId > 0 { 282 | v += (chainId * 2 + 8) & 0xFF; 283 | } 284 | ``` 285 | 286 | #### Substrate Signature 287 | 288 | TODO 289 | 290 | ## Copyright 291 | 292 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 293 | --------------------------------------------------------------------------------