|
| 1 | +--- |
| 2 | +simd: "0075" |
| 3 | +title: Precompile for verifying secp256r1 sig. |
| 4 | +authors: |
| 5 | + - Orion (Bunkr) |
| 6 | + - Jstnw (Bunkr) |
| 7 | + - Dean (Web3 Builders Alliance) |
| 8 | +category: Standard |
| 9 | +type: Core |
| 10 | +status: Draft |
| 11 | +created: 2024-02-27 |
| 12 | +feature: (fill in with feature tracking issues once accepted) |
| 13 | +supersedes: "0048" |
| 14 | +--- |
| 15 | + |
| 16 | +## Summary |
| 17 | + |
| 18 | +Adding a precompile to support the verification of signatures generated on |
| 19 | +the secp256r1 curve. Analogous to the support for secp256k1 and ed25519 |
| 20 | +signatures that already exists in form of the |
| 21 | +`KeccakSecp256k11111111111111111111111111111` and |
| 22 | +`Ed25519SigVerify111111111111111111111111111` precompiles. |
| 23 | + |
| 24 | +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL |
| 25 | +NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and |
| 26 | +"OPTIONAL" in this document are to be interpreted as described in |
| 27 | +RFC 2119. |
| 28 | + |
| 29 | +## Motivation |
| 30 | + |
| 31 | +Solana has the opportunity to leverage the secure element of users' existing |
| 32 | +mobile devices to support more user-friendly self-custodial security solutions. |
| 33 | +The status quo of air-gapping signing with a hardware wallet currently requires |
| 34 | +specialty hardware and still represents a single point of failure. Multi-signature |
| 35 | +wallets provide enhanced security through multi-party signing, however the UX |
| 36 | +is cumbersome due to the need to sign transactions multiple times and manage |
| 37 | +multiple seed phrases. A much more ergonomic approach combining the best of |
| 38 | +these two solutions on generalised mobile hardware could be achieved by adding |
| 39 | +support for secp256r1 signatures. |
| 40 | + |
| 41 | +There are already several standardised implementations of this, such as Passkeys |
| 42 | +and WebAuthn. These solutions leverage Apple's Secure Enclave and Android Keystore |
| 43 | +to enable users to save keypairs associated to different services natively on |
| 44 | +the secure element of their mobile devices. To authenticate with |
| 45 | +those services, the user uses their biometrics to sign a message with the stored |
| 46 | +private key. |
| 47 | + |
| 48 | +While originally intended to solve for password-less authentication in Web2 |
| 49 | +applications, WebAuthn and Passkeys also make an excellent candidate for on-chain |
| 50 | +second-factor authentication. Beyond simply securing funds, there are also many |
| 51 | +other potential beneficial abstractions that could make use of the simple UX |
| 52 | +they provide. |
| 53 | + |
| 54 | +Although WebAuthn supports the following curves: |
| 55 | + |
| 56 | +- P-256 |
| 57 | +- P-384 |
| 58 | +- P-521 |
| 59 | +- ed25519 |
| 60 | + |
| 61 | +P-256 is the only one supported by both Android & MacOS/iOS (MacOS/iOS being the |
| 62 | +more restrictive of the two), hence the goal being to implement secp256r1 signature |
| 63 | +verification |
| 64 | + |
| 65 | +General Documentation: |
| 66 | + |
| 67 | +[WebAuthn](https://webauthn.io/) |
| 68 | + |
| 69 | +[Passkeys](https://fidoalliance.org/passkeys/) |
| 70 | + |
| 71 | +**Note: P-256 / secp256r1 / prime256v1 are used interchangably in this document |
| 72 | +as they represent the same elliptic curve. The choice of nomenclature depends on |
| 73 | +what RFC or SEC document is being referenced.** |
| 74 | + |
| 75 | +## Alternatives Considered |
| 76 | + |
| 77 | +We have discussed the following alternatives: |
| 78 | + |
| 79 | +1.) Realising signature verification with a syscall similar |
| 80 | +to `secp256k1_recover()` instead of a precompile. This would ease |
| 81 | +integration for developers, since no instruction introspection would be |
| 82 | +required when utilizing the syscall. This is still a valid consideration. |
| 83 | + |
| 84 | +2.) Realising signature verification through and on-chain sBPF implemenation. On |
| 85 | +a local validator a single signature verification consumes ≈42M compute units. |
| 86 | +A possibility would be to split the verification into multiple transactions. |
| 87 | +This would most probably require off-chain infrastructure to crank the process |
| 88 | +or carry higher transaction fees for the end user. (similar to the current elusiv |
| 89 | +protocol private transfer) |
| 90 | +We feel this alternative directly contradicts and impinges on the main upside of |
| 91 | +passkeys, which is the incredible UX and ease of use to the end user. |
| 92 | + |
| 93 | +3.) Allowing for high-S signatures was considered, however the pitfalls |
| 94 | +of signature malleability are too great to leave open to implementation. |
| 95 | + |
| 96 | +4.) Allowing for uncompressed keys was considered, however as we are already |
| 97 | +taking an opinionated stance on signature malleability, it makes sense to |
| 98 | +also take an opinionated stance on public key encoding. |
| 99 | + |
| 100 | +## New Terminology |
| 101 | + |
| 102 | +None |
| 103 | + |
| 104 | +## Detailed Design |
| 105 | + |
| 106 | +The precompile's purpose is to verify signatures using ECDSA-256. |
| 107 | +(denoted in [RFC6460](https://www.ietf.org/rfc/rfc6460.txt) as |
| 108 | +ECDSA using the NIST P-256 curve and the SHA-256 hashing algorithm) |
| 109 | + |
| 110 | +Apart from the RFC mandated implementation the precompile must additionally take |
| 111 | +an opinionated stance on signature malleability. |
| 112 | + |
| 113 | +### Signature Malleability |
| 114 | + |
| 115 | +Due to X axis symmetry along the elliptic curve, for any ECDSA signature |
| 116 | +`(r, s)`, there also exists a valid signature `(r, n - s)`, where `n` is the |
| 117 | +order of the curve. This introduces "s malleability", allowing an attacker |
| 118 | +to produce an alternative version of `s` without invalidating the signature. |
| 119 | + |
| 120 | +The pitfalls of this in authentication systems can be particularly perilous, |
| 121 | +opening up certain implementations to signature replay attacks over the same |
| 122 | +message by simply flipping the `s` value over the curve. |
| 123 | + |
| 124 | +As the primary goal of the `secp256r1` program is secure signature validation |
| 125 | +for authentication purposes, the precompile must mitigate these attacks |
| 126 | +by enforcing the usage of `lowS` values, in which `s <= n/2`. |
| 127 | + |
| 128 | +As such, the program must immediately fail upon the detection of any |
| 129 | +signature that includes a `highS` value. This prevents any accidental |
| 130 | +succeptibility to signature malleability attacks. |
| 131 | + |
| 132 | +Note: The existing `secp256k1` precompile makes no attempt attempt to mitigate |
| 133 | +s malleability, as doing so would go against its primary goal of achieving |
| 134 | +`ecrecover` parity with EVM. |
| 135 | + |
| 136 | +### Implementation |
| 137 | + |
| 138 | +### Program |
| 139 | + |
| 140 | +ID: `Secp256r1SigVerify1111111111111111111111111` |
| 141 | + |
| 142 | +In accordance with [SIMD |
| 143 | +0152](https://github.com/solana-foundation/solana-improvement-documents/pull/152) |
| 144 | +the programs ```verify``` instruction must accept the following data: |
| 145 | + |
| 146 | +In Pseudocode: |
| 147 | + |
| 148 | +``` |
| 149 | +struct Secp256r1SigVerifyInstruction { |
| 150 | + num_signatures: uint8 LE, // Number of signatures to verify |
| 151 | + padding: uint8 LE, // Single byte padding |
| 152 | + offsets: Array<Secp256r1SignatureOffsets>, // Array of offset structs |
| 153 | + additionalData?: Bytes, // Optional additional data, e.g. |
| 154 | + // signatures included in the same |
| 155 | + // instruction |
| 156 | +} |
| 157 | +Note: Array<Secp256r1SignatureOffsets> does not contain any length prefixes or |
| 158 | +padding between elements. |
| 159 | +
|
| 160 | +struct Secp256r1SignatureOffsets { |
| 161 | + signature_offset: uint16 LE, // Offset to signature |
| 162 | + signature_instruction_index: uint16 LE, // Instruction index to signature |
| 163 | + public_key_offset: uint16 LE, // Offset to public key |
| 164 | + public_key_instruction_index: uint16 LE, // Instruction index to public key |
| 165 | + message_offset: uint16 LE, // Offset to start of message data |
| 166 | + message_length: uint16 LE, // Size of message data |
| 167 | + message_instruction_index: uint16 LE, // Instruction index to message |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +Up to 8 signatures can be verified. If any of the signatures fail to verify, |
| 172 | +an error must be returned. |
| 173 | + |
| 174 | +In accordance with [SIMD |
| 175 | +0152](https://github.com/solana-foundation/solana-improvement-documents/pull/152) |
| 176 | +the behavior of the program must be as follows: |
| 177 | + |
| 178 | +1. If instruction `data` is empty, return error. |
| 179 | +2. The first byte of `data` is the number of signatures `num_signatures`. |
| 180 | +3. If `num_signatures` is 0, return error. |
| 181 | +4. Expect (enough bytes of `data` for) `num_signatures` instances of |
| 182 | + `Secp256r1SignatureOffsets`. |
| 183 | +5. For each signature: |
| 184 | + a. Read `offsets`: an instance of `Secp256r1SignatureOffsets` |
| 185 | + b. Based on the `offsets`, retrieve `signature`, `public_key`, and |
| 186 | + `message` bytes. If any of the three fails, return error. |
| 187 | + c. Invoke the actual `sigverify` function. If it fails, return error. |
| 188 | + |
| 189 | +To retrieve `signature`, `public_key`, and `message`: |
| 190 | + |
| 191 | +1. Get the `instruction_index`-th `instruction_data` |
| 192 | + - The special value `0xFFFF` means "current instruction" |
| 193 | + - If the index is invalid, return Error |
| 194 | +2. Return `length` bytes starting from `offset` |
| 195 | + - If this exceeds the `instruction_data` length, return Error |
| 196 | + |
| 197 | +Note that fields (offsets) can overlap, for example the same public key or |
| 198 | +message can be referred to by multiple instances of `Secp256r1SignatureOffsets`. |
| 199 | + |
| 200 | +If the precompile `verify` function returns any error, the whole transaction |
| 201 | +should fail. Therefore, the type of error is irrelevant and is left as an |
| 202 | +implementation detail. |
| 203 | + |
| 204 | +The instruction processing logic must follow the pseudocode below: |
| 205 | + |
| 206 | +``` |
| 207 | +/// `data` is the secp256r1 program's instruction data. `instruction_datas` is |
| 208 | +/// the full slice of instruction datas for all instructions in the transaction, |
| 209 | +/// including the secp256r1 program's instruction data. |
| 210 | +
|
| 211 | +/// length_of_data is the length of `data` |
| 212 | +
|
| 213 | +/// SERIALIZED_OFFSET_STRUCT_SIZE is the length of the serialized |
| 214 | +/// Secp256r1SignatureOffsets struct |
| 215 | +
|
| 216 | +/// SERIALIZED_PUBLIC_KEY_LENGTH and SERIALIZED_SIGNATURE_LENGTH represent the |
| 217 | +/// length of the serialized public key and signature respectively |
| 218 | +
|
| 219 | +function verify() { |
| 220 | + if length_of_data == 0 { |
| 221 | + return Error |
| 222 | + } |
| 223 | + num_signatures = data[0] |
| 224 | + if num_signatures == 0 && length_of_data > 1 { |
| 225 | + return Error |
| 226 | + } |
| 227 | + if length_of_data < (num_signatures * SERIALIZED_OFFSET_STRUCT_SIZE + 2) { |
| 228 | + return Error |
| 229 | + } |
| 230 | + all_tx_data = { data, instruction_datas } |
| 231 | + data_start_position = 2 |
| 232 | +
|
| 233 | + for i in 0..num_signatures { |
| 234 | + offsets = (Secp256r1SignatureOffsets) |
| 235 | + all_tx_data.data[data_start_position..data_start_position + SERIALIZED_OFFSET_STRUCT_SIZE] |
| 236 | + data_position += SERIALIZED_OFFSET_STRUCT_SIZE |
| 237 | +
|
| 238 | + signature = get_data_slice(all_tx_data, |
| 239 | + offsets.signature_instruction_index, |
| 240 | + offsets.signature_offset |
| 241 | + signature_length) |
| 242 | + if !signature { |
| 243 | + return Error |
| 244 | + } |
| 245 | +
|
| 246 | + public_key = get_data_slice(all_tx_data, |
| 247 | + offsets.public_key_instruction_index, |
| 248 | + offsets.public_key_offset, |
| 249 | + SERIALIZED_PUBLIC_KEY_LENGTH) |
| 250 | + if !public_key { |
| 251 | + return Error |
| 252 | + } |
| 253 | +
|
| 254 | + message = get_data_slice(all_tx_data, |
| 255 | + offsets.message_instruction_index, |
| 256 | + offsets.message_offset |
| 257 | + offsets.message_length) |
| 258 | + if !message { |
| 259 | + return Error |
| 260 | + } |
| 261 | +
|
| 262 | + // sigverify includes validating signature and public_key |
| 263 | + // the additional highS check is done here |
| 264 | + if signature_S == highS { |
| 265 | + return Error |
| 266 | + } |
| 267 | + result = sigverify(signature, public_key, message) |
| 268 | + if result != Success { |
| 269 | + return Error |
| 270 | + } |
| 271 | + } |
| 272 | + return Success |
| 273 | +} |
| 274 | +// This function is re-used across precompiles in accordance with SIMD-0152 |
| 275 | +fn get_data_slice(all_tx_data, instruction_index, offset, length) { |
| 276 | + // Get the right instruction_data |
| 277 | + if instruction_index == 0xFFFF { |
| 278 | + instruction_data = all_tx_data.data |
| 279 | + } else { |
| 280 | + if instruction_index >= num_instructions { |
| 281 | + return Error |
| 282 | + } |
| 283 | + instruction_data = all_tx_data.instruction_datas[instruction_index] |
| 284 | + } |
| 285 | +
|
| 286 | + start = offset |
| 287 | + end = offset + length |
| 288 | + if end > instruction_data_length { |
| 289 | + return Error |
| 290 | + } |
| 291 | +
|
| 292 | + return instruction_data[start..end] |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +Additonally the precompile's core `verify` function must be constructed in |
| 297 | +accordance with the structure outlined in [sdk/src/precompiles.rs](https://github.com/solana-labs/solana/blob/9ffbe2afd8ab5b972c4ad87d758866a3e1bb87fb/sdk/src/precompiles.rs). |
| 298 | + |
| 299 | +### Compute Cost / Efficiency |
| 300 | + |
| 301 | +Benchmarking and compute cost calculations must be done in accordance with [SIMD-0121](https://github.com/solana-foundation/solana-improvement-documents/pull/121) |
| 302 | + |
| 303 | +Additionally, comparisons to existing precompiles should be done to check for |
| 304 | +comperable efficiency. |
| 305 | + |
| 306 | +## Impact |
| 307 | + |
| 308 | +Would enable the on-chain usage of Passkeys and the WebAuthn Standard, and |
| 309 | +turn the vast majority of modern smartphones into native hardware wallets. |
| 310 | + |
| 311 | +By extension, this would also enable the creation of account abstractions and |
| 312 | +forms of Two-Factor Authentication around those keypairs. |
| 313 | + |
| 314 | +## Security Considerations |
| 315 | + |
| 316 | +The following security considerations must be made for the |
| 317 | +implementation of ECDSA over NIST P-256. |
| 318 | + |
| 319 | +### Curve |
| 320 | + |
| 321 | +The curve parameters for NIST P-256/secp256r1/prime256v1 are |
| 322 | +outlined in the [SEC2](https://www.secg.org/SEC2-Ver-1.0.pdf#page=21) |
| 323 | +document in Section 2.7.2 |
| 324 | + |
| 325 | +### Point Encoding/Decoding |
| 326 | + |
| 327 | +The precompile must accept SEC1 encoded points in compressed form. |
| 328 | +The encoding and decoding of these is outlined in sections |
| 329 | +`2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion` |
| 330 | +and `2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion` |
| 331 | +found in [SEC1](https://www.secg.org/sec1-v2.pdf#page=16). |
| 332 | + |
| 333 | +The SEC1 encoded EC point P = (x_p, y_p) in compressed form consists |
| 334 | +of 33 bytes (octets). The first byte of 02_16 / 03_16 signifies a |
| 335 | +compressed point, as well as whether y_p is odd or even. The remaining |
| 336 | +32 bytes represent x_p converted into a 32 octet string. |
| 337 | + |
| 338 | +While SEC1 encoded uncompressed points could also be used, |
| 339 | +due to their larger size of 65 bytes, the ease of transformation |
| 340 | +between uncompressed and compressed points, and the vast majority |
| 341 | +of applications exclusively making use of compressed points, it |
| 342 | +seems a reasonable consideration to save 32 bytes of instruction |
| 343 | +data with a protocol that only accepts compressed points. |
| 344 | + |
| 345 | +### ECDSA / Signature Verification |
| 346 | + |
| 347 | +The precompile must implement the `Verifying Operation` outlined in |
| 348 | +[SEC1](https://www.secg.org/sec1-v2.pdf#page=52) |
| 349 | +in Section 4.1.4 as well as in the |
| 350 | +[Digital Signature Standard (DSS)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf#page=36) |
| 351 | +document in Section 6.4.2. |
| 352 | + |
| 353 | +A multitude of test vectors to verify correctness can |
| 354 | +be found in |
| 355 | +[RFC6979](https://datatracker.ietf.org/doc/html/rfc6979#appendix-A.2.5) |
| 356 | +in Section A.2.5 as well as at the |
| 357 | +[NIST CAVP](https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/digital-signatures#ecdsa2vs) |
| 358 | +(Cryptographic Algorithm Validation Program) |
| 359 | + |
| 360 | +### General |
| 361 | + |
| 362 | +As multiple other clients are being developed, it is imperative that there is |
| 363 | +bit-level reproducibility between the precompile implementations, especially |
| 364 | +with regard to cryptographic operations. Any discrepancy between implementations |
| 365 | +could cause a fork and or a chain halt. |
| 366 | + |
| 367 | +As such we would propose the following: |
| 368 | + |
| 369 | +- Development of a thorough test suite that includes all test vectors as well |
| 370 | + as tests from the |
| 371 | + [Wycheproof Project](https://github.com/google/wycheproof#project-wycheproof) |
| 372 | + |
| 373 | +- Maintaining active communication with other clients to ensure parity and to |
| 374 | + support potential changes if they arise. |
| 375 | + |
| 376 | +## Backwards Compatibility |
| 377 | + |
| 378 | +Transactions using the instruction could not be used on Solana versions which don't |
| 379 | +implement this feature. A Feature gate should be used to enable this feature |
| 380 | +when the majority of the cluster is using the required version. Transactions |
| 381 | +that do not use this feature are not impacted. |
0 commit comments