Skip to content

Commit 0c0c5ee

Browse files
Transpile d8e799db
1 parent 4c078ae commit 0c0c5ee

File tree

7 files changed

+4001
-1
lines changed

7 files changed

+4001
-1
lines changed

.changeset/curvy-crabs-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`RSA`: Library to verify signatures according to RFC 8017 Signature Verification Operation

contracts/mocks/StatelessUpgradeable.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProo
2727
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
2828
import {Panic} from "@openzeppelin/contracts/utils/Panic.sol";
2929
import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
30+
import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol";
3031
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
3132
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
3233
import {ShortStrings} from "@openzeppelin/contracts/utils/ShortStrings.sol";

docs/modules/ROOT/pages/utilities.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Here are some of the more popular ones.
88

99
=== Checking Signatures On-Chain
1010

11+
At a high level, signatures are a set of cryptographic algorithms that allow for a _signer_ to prove himself owner of a _private key_ used to authorize an piece of information (generally a transaction or `UserOperation`). Natively, the EVM supports the Elliptic Curve Digital Signature Algorithm (https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm[ECDSA]) using the secp256r1 field, however other signature algorithms such as RSA are supported.
12+
13+
==== Ethereum Signatures (secp256r1)
14+
1115
xref:api:utils.adoc#ECDSA[`ECDSA`] provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign[`web3.eth.sign`], and are a 65 byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`.
1216

1317
The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix `\x19Ethereum Signed Message:\n`, so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`].
@@ -26,6 +30,32 @@ function _verify(bytes32 data, bytes memory signature, address account) internal
2630

2731
WARNING: Getting signature verification right is not trivial: make sure you fully read and understand xref:api:utils.adoc#MessageHashUtils[`MessageHashUtils`]'s and xref:api:utils.adoc#ECDSA[`ECDSA`]'s documentation.
2832

33+
==== RSA
34+
35+
RSA a public-key cryptosystem that was popularized by corporate and governmental public key infrastructures (https://en.wikipedia.org/wiki/Public_key_infrastructure[PKIs]) and https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions[DNSSEC].
36+
37+
This cryptosystem consists of using a private key that's the product of 2 large prime numbers. The message is signed by applying a modular exponentiation to its hash (commonly SHA256), where both the exponent and modulus compose the public key of the signer.
38+
39+
RSA signatures are known for being less efficient than elliptic curve signatures given the size of the keys, which are big compared to ECDSA keys with the same security level. Using plain RSA is considered unsafe, this is why the implementation uses the `EMSA-PKCS1-v1_5` encoding method from https://datatracker.ietf.org/doc/html/rfc8017[RFC8017] to include padding to the signature.
40+
41+
To verify a signature using RSA, you can leverage the xref:api:utils.adoc#RSA[`RSA`] library that exposes a method for verifying RSA with the PKCS 1.5 standard:
42+
43+
[source,solidity]
44+
----
45+
using RSA for bytes32;
46+
47+
function _verify(
48+
bytes32 data,
49+
bytes memory signature,
50+
bytes memory e,
51+
bytes memory n
52+
) internal pure returns (bool) {
53+
return data.pkcs1(signature, e, n);
54+
}
55+
----
56+
57+
IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message.
58+
2959
=== Verifying Merkle Proofs
3060

3161
Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g. for airdrops) and other advanced use cases.

test/utils/cryptography/RSA.helper.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
4+
module.exports = function* parse(file) {
5+
const cache = {};
6+
const data = fs.readFileSync(path.resolve(__dirname, file), 'utf8');
7+
for (const line of data.split('\r\n')) {
8+
const groups = line.match(/^(?<key>\w+) = (?<value>\w+)(?<extra>.*)$/)?.groups;
9+
if (groups) {
10+
const { key, value, extra } = groups;
11+
cache[key] = value;
12+
if (groups.key === 'Result') {
13+
yield Object.assign({ extra: extra.trim() }, cache);
14+
}
15+
}
16+
}
17+
};

test/utils/cryptography/RSA.test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
const parse = require('./RSA.helper');
6+
7+
async function fixture() {
8+
return { mock: await ethers.deployContract('$RSA') };
9+
}
10+
11+
describe('RSA', function () {
12+
beforeEach(async function () {
13+
Object.assign(this, await loadFixture(fixture));
14+
});
15+
16+
// Load test cases from file SigVer15_186-3.rsp from:
17+
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-2rsatestvectors.zip
18+
describe('SigVer15_186-3.rsp tests', function () {
19+
for (const test of parse('SigVer15_186-3.rsp')) {
20+
const { length } = Buffer.from(test.S, 'hex');
21+
22+
/// For now, RSA only supports digest that are 32bytes long. If we ever extend that, we can use these hashing functions for @noble:
23+
// const { sha1 } = require('@noble/hashes/sha1');
24+
// const { sha224, sha256 } = require('@noble/hashes/sha256');
25+
// const { sha384, sha512 } = require('@noble/hashes/sha512');
26+
27+
if (test.SHAAlg === 'SHA256') {
28+
const result = test.Result === 'P';
29+
30+
it(`signature length ${length} ${test.extra} ${result ? 'works' : 'fails'}`, async function () {
31+
const data = '0x' + test.Msg;
32+
const sig = '0x' + test.S;
33+
const exp = '0x' + test.e;
34+
const mod = '0x' + test.n;
35+
36+
expect(await this.mock.$pkcs1(ethers.sha256(data), sig, exp, mod)).to.equal(result);
37+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.equal(result);
38+
});
39+
}
40+
}
41+
});
42+
43+
describe('others tests', function () {
44+
it('openssl', async function () {
45+
const data = ethers.toUtf8Bytes('hello world');
46+
const sig =
47+
'0x079bed733b48d69bdb03076cb17d9809072a5a765460bc72072d687dba492afe951d75b814f561f253ee5cc0f3d703b6eab5b5df635b03a5437c0a5c179309812f5b5c97650361c645bc99f806054de21eb187bc0a704ed38d3d4c2871a117c19b6da7e9a3d808481c46b22652d15b899ad3792da5419e50ee38759560002388';
48+
const exp =
49+
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001';
50+
const mod =
51+
'0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d';
52+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true;
53+
});
54+
55+
// According to RFC4055, pg.5 and RFC8017, pg. 64, for SHA-1, and the SHA-2 family,
56+
// the algorithm parameter has to be NULL and both explicit NULL parameter and implicit
57+
// NULL parameter (ie, absent NULL parameter) are considered to be legal and equivalent.
58+
it('rfc8017 implicit null parameter', async function () {
59+
const data = ethers.toUtf8Bytes('hello world!');
60+
const sig =
61+
'0xa0073057133ff3758e7e111b4d7441f1d8cbe4b2dd5ee4316a14264290dee5ed7f175716639bd9bb43a14e4f9fcb9e84dedd35e2205caac04828b2c053f68176d971ea88534dd2eeec903043c3469fc69c206b2a8694fd262488441ed8852280c3d4994e9d42bd1d575c7024095f1a20665925c2175e089c0d731471f6cc145404edf5559fd2276e45e448086f71c78d0cc6628fad394a34e51e8c10bc39bfe09ed2f5f742cc68bee899d0a41e4c75b7b80afd1c321d89ccd9fe8197c44624d91cc935dfa48de3c201099b5b417be748aef29248527e8bbb173cab76b48478d4177b338fe1f1244e64d7d23f07add560d5ad50b68d6649a49d7bc3db686daaa7';
62+
const exp = '0x03';
63+
const mod =
64+
'0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7';
65+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true;
66+
});
67+
68+
it('returns false for a very short n', async function () {
69+
const data = ethers.toUtf8Bytes('hello world!');
70+
const sig = '0x0102';
71+
const exp = '0x03';
72+
const mod = '0x0405';
73+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
74+
});
75+
76+
it('returns false for a signature with different length to n', async function () {
77+
const data = ethers.toUtf8Bytes('hello world!');
78+
const sig = '0x00112233';
79+
const exp = '0x03';
80+
const mod =
81+
'0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7';
82+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
83+
});
84+
85+
it('returns false if s >= n', async function () {
86+
// this is the openssl example where sig has been replaced by sig + mod
87+
const data = ethers.toUtf8Bytes('hello world');
88+
const sig =
89+
'0xe6dacb53450242618b3e502a257c08acb44b456c7931988da84f0cda8182b435d6d5453ac1e72b07c7dadf2747609b7d544d15f3f14081f9dbad9c48b7aa78d2bdafd81d630f19a0270d7911f4ec82b171e9a95889ffc9e740dc9fac89407a82d152ecb514967d4d9165e67ce0d7f39a3082657cdfca148a5fc2b3a7348c4795';
90+
const exp =
91+
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001';
92+
const mod =
93+
'0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d';
94+
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)