|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import {Math} from "../math/Math.sol"; |
| 5 | + |
| 6 | +/** |
| 7 | + * @dev RSA PKCS#1 v1.5 signature verification implementation according to https://datatracker.ietf.org/doc/html/rfc8017[RFC8017]. |
| 8 | + * |
| 9 | + * This library supports PKCS#1 v1.5 padding to avoid malleability via chosen plaintext attacks in practical implementations. |
| 10 | + * The padding follows the EMSA-PKCS1-v1_5-ENCODE encoding definition as per section 9.2 of the RFC. This padding makes |
| 11 | + * RSA semanticaly secure for signing messages. |
| 12 | + * |
| 13 | + * Inspired by https://github.com/adria0/SolRsaVerify[Adrià Massanet's work] |
| 14 | + */ |
| 15 | +library RSA { |
| 16 | + /** |
| 17 | + * @dev Same as {pkcs1} but using SHA256 to calculate the digest of `data`. |
| 18 | + */ |
| 19 | + function pkcs1Sha256( |
| 20 | + bytes memory data, |
| 21 | + bytes memory s, |
| 22 | + bytes memory e, |
| 23 | + bytes memory n |
| 24 | + ) internal view returns (bool) { |
| 25 | + return pkcs1(sha256(data), s, e, n); |
| 26 | + } |
| 27 | + |
| 28 | + /** |
| 29 | + * @dev Verifies a PKCSv1.5 signature given a digest according the verification |
| 30 | + * method described in https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.2[section 8.2.2 of RFC8017]. |
| 31 | + * |
| 32 | + * IMPORTANT: Although this function allows for it, using n of length 1024 bits is considered unsafe. |
| 33 | + * Consider using at least 2048 bits. |
| 34 | + * |
| 35 | + * WARNING: PKCS#1 v1.5 allows for replayability given the message may contain arbitrary optional parameters in the |
| 36 | + * DigestInfo. Consider using an onchain nonce or unique identifier to include in the message to prevent replay attacks. |
| 37 | + * |
| 38 | + * @param digest the digest to verify |
| 39 | + * @param s is a buffer containing the signature |
| 40 | + * @param e is the exponent of the public key |
| 41 | + * @param n is the modulus of the public key |
| 42 | + */ |
| 43 | + function pkcs1(bytes32 digest, bytes memory s, bytes memory e, bytes memory n) internal view returns (bool) { |
| 44 | + unchecked { |
| 45 | + // cache and check length |
| 46 | + uint256 length = n.length; |
| 47 | + if ( |
| 48 | + length < 0x40 || // PKCS#1 padding is slightly less than 0x40 bytes at the bare minimum |
| 49 | + length != s.length // signature must have the same length as the finite field |
| 50 | + ) { |
| 51 | + return false; |
| 52 | + } |
| 53 | + |
| 54 | + // Verify that s < n to ensure there's only one valid signature for a given message |
| 55 | + for (uint256 i = 0; i < length; i += 0x20) { |
| 56 | + uint256 p = Math.min(i, length - 0x20); |
| 57 | + bytes32 sp = _unsafeReadBytes32(s, p); |
| 58 | + bytes32 np = _unsafeReadBytes32(n, p); |
| 59 | + if (sp < np) { |
| 60 | + // s < n in the upper bits (everything before is equal) → s < n globally: ok |
| 61 | + break; |
| 62 | + } else if (sp > np || p == length - 0x20) { |
| 63 | + // s > n in the upper bits (everything before is equal) → s > n globally: fail |
| 64 | + // or |
| 65 | + // s = n and we are looking at the lower bits → s = n globally: fail |
| 66 | + return false; |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + // RSAVP1 https://datatracker.ietf.org/doc/html/rfc8017#section-5.2.2 |
| 71 | + // The previous check guarantees that n > 0. Therefore modExp cannot revert. |
| 72 | + bytes memory buffer = Math.modExp(s, e, n); |
| 73 | + |
| 74 | + // Check that buffer is well encoded: |
| 75 | + // buffer ::= 0x00 | 0x01 | PS | 0x00 | DigestInfo |
| 76 | + // |
| 77 | + // With |
| 78 | + // - PS is padding filled with 0xFF |
| 79 | + // - DigestInfo ::= SEQUENCE { |
| 80 | + // digestAlgorithm AlgorithmIdentifier, |
| 81 | + // [optional algorithm parameters] |
| 82 | + // digest OCTET STRING |
| 83 | + // } |
| 84 | + |
| 85 | + // Get AlgorithmIdentifier from the DigestInfo, and set the config accordingly |
| 86 | + // - params: includes 00 + first part of DigestInfo |
| 87 | + // - mask: filter to check the params |
| 88 | + // - offset: length of the suffix (including digest) |
| 89 | + bytes32 params; // 0x00 | DigestInfo |
| 90 | + bytes32 mask; |
| 91 | + uint256 offset; |
| 92 | + |
| 93 | + // Digest is expected at the end of the buffer. Therefore if NULL param is present, |
| 94 | + // it should be at 32 (digest) + 2 bytes from the end. To those 34 bytes, we add the |
| 95 | + // OID (9 bytes) and its length (2 bytes) to get the position of the DigestInfo sequence, |
| 96 | + // which is expected to have a length of 0x31 when the NULL param is present or 0x2f if not. |
| 97 | + if (bytes1(_unsafeReadBytes32(buffer, length - 50)) == 0x31) { |
| 98 | + offset = 0x34; |
| 99 | + // 00 (1 byte) | SEQUENCE length (0x31) = 3031 (2 bytes) | SEQUENCE length (0x0d) = 300d (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes) |
| 100 | + // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes) |
| 101 | + params = 0x003031300d060960864801650304020105000420000000000000000000000000; |
| 102 | + mask = 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000; // (20 bytes) |
| 103 | + } else if (bytes1(_unsafeReadBytes32(buffer, length - 48)) == 0x2F) { |
| 104 | + offset = 0x32; |
| 105 | + // 00 (1 byte) | SEQUENCE length (0x2f) = 302f (2 bytes) | SEQUENCE length (0x0b) = 300b (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes) |
| 106 | + // SHA256 OID = 608648016503040201 (9 bytes) | NULL = <implicit> | OCTET_STRING length (0x20) = 0420 (2 bytes) |
| 107 | + params = 0x00302f300b060960864801650304020104200000000000000000000000000000; |
| 108 | + mask = 0xffffffffffffffffffffffffffffffffffff0000000000000000000000000000; // (18 bytes) |
| 109 | + } else { |
| 110 | + // unknown |
| 111 | + return false; |
| 112 | + } |
| 113 | + |
| 114 | + // Length is at least 0x40 and offset is at most 0x34, so this is safe. There is always some padding. |
| 115 | + uint256 paddingEnd = length - offset; |
| 116 | + |
| 117 | + // The padding has variable (arbitrary) length, so we check it byte per byte in a loop. |
| 118 | + // This is required to ensure non-malleability. Not checking would allow an attacker to |
| 119 | + // use the padding to manipulate the message in order to create a valid signature out of |
| 120 | + // multiple valid signatures. |
| 121 | + for (uint256 i = 2; i < paddingEnd; ++i) { |
| 122 | + if (bytes1(_unsafeReadBytes32(buffer, i)) != 0xFF) { |
| 123 | + return false; |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // All the other parameters are small enough to fit in a bytes32, so we can check them directly. |
| 128 | + return |
| 129 | + bytes2(0x0001) == bytes2(_unsafeReadBytes32(buffer, 0x00)) && // 00 | 01 |
| 130 | + // PS was checked in the loop |
| 131 | + params == _unsafeReadBytes32(buffer, paddingEnd) & mask && // DigestInfo |
| 132 | + // Optional parameters are not checked |
| 133 | + digest == _unsafeReadBytes32(buffer, length - 0x20); // Digest |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + /// @dev Reads a bytes32 from a bytes array without bounds checking. |
| 138 | + function _unsafeReadBytes32(bytes memory array, uint256 offset) private pure returns (bytes32 result) { |
| 139 | + // Memory safetiness is guaranteed as long as the provided `array` is a Solidity-allocated bytes array |
| 140 | + // and `offset` is within bounds. This is the case for all calls to this private function from {pkcs1}. |
| 141 | + assembly ("memory-safe") { |
| 142 | + result := mload(add(add(array, 0x20), offset)) |
| 143 | + } |
| 144 | + } |
| 145 | +} |
0 commit comments