Skip to content

Commit

Permalink
Predict deterministic RSA signer address sync
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestognw committed May 19, 2024
1 parent 6ac94c0 commit 2c82862
Show file tree
Hide file tree
Showing 8 changed files with 603 additions and 212 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,30 @@ assert(isValid); // true
## RSASHA256SafeSigner

A variant of `RSASHA256Signer` that signs messages as a Safe owner, which has a different signature format with a static prefix followed by a dynamic part where the signature is stored.

### Usage

Creating and using a Safe signer works the same as if making a regular signer, but the signature format follows the Safe's [signature encoding](https://docs.safe.global/advanced/smart-account-signatures#encoding)

```typescript
import { RSASHA256SafeSigner } from "@plumaa/signer";
import { pki, md } from "node-forge";

// 1. Generate a new RSA keypair or load an existing one
const keypair = pki.rsa.generateKeyPair(2048);

// 2. Create a signer with the keypair
const signer = new RSASHA256SafeSigner(keypair);

// 3. Sign a message
const message = "Hello, world!";
const signature = await signer.signMessage({ message });

// 4. Verify the signature
const isValid = keypair.publicKey.verify(
signature,
md.create().update(message).digest().bytes()
);

assert(isValid); // true
```
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@
"@fast-check/ava": "^1.2.1",
"@types/node": "^20.12.7",
"@types/node-forge": "^1.3.11",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"c8": "^9.1.0",
"sinon": "^17.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
Expand Down
110 changes: 0 additions & 110 deletions src/abi/RSASignerFactory.ts

This file was deleted.

262 changes: 262 additions & 0 deletions src/artifacts/RSASigner.ts

Large diffs are not rendered by default.

254 changes: 254 additions & 0 deletions src/artifacts/RSASignerFactory.ts

Large diffs are not rendered by default.

68 changes: 9 additions & 59 deletions src/rsa-sha256-signer.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { with0x, without0x } from "./utils";
import { predictRSASignerAddress, with0x, without0x } from "./utils";
import { testProp, fc } from "@fast-check/ava";
import { md, pki, util } from "node-forge";
import { RSASHA256Signer } from "./";
import sinon from "sinon";
import * as utils from "./utils";
import { Client, hashMessage, hashTypedData, slice, toHex } from "viem";
import { hashMessage, hashTypedData, slice, toHex } from "viem";

const RSAEOA = fc.gen().map(() => {
const owner = pki.rsa.generateKeyPair(2048);
return {
signer: new RSASHA256Signer(
owner,
"0x0000000000000000000000000000000000000000"
),
signer: new RSASHA256Signer(owner),
owner,
};
});
Expand All @@ -22,57 +17,12 @@ const addressArbitrary = fc.hexaString({
maxLength: 40,
});

const getRSASignerFactoryMock = sinon.mock(utils);

testProp(
"#from creates a signer with an address from the universal factory",
[addressArbitrary],
async (t, add) => {
const addressWith0x = with0x(add);
const predictDeterministicAddressStub = sinon
.stub()
.resolves(addressWith0x);
const clientStub = sinon.stub();
const RSASignerFactoryMockExpectation = getRSASignerFactoryMock
.expects("getRSASignerFactory")
.once()
.withArgs(clientStub)
.returns({
read: {
predictDeterministicAddress: predictDeterministicAddressStub,
},
});
const signer = await RSASHA256Signer.from(
pki.rsa.generateKeyPair(2048),
clientStub as unknown as Client
);
t.is(signer.address, addressWith0x);
t.true(
predictDeterministicAddressStub.calledOnceWith([
{
exponent: with0x(signer.rsaPublicKey.e.toString(16)),
modulus: with0x(signer.rsaPublicKey.n.toString(16)),
},
utils.factory,
])
);
t.true(RSASignerFactoryMockExpectation.calledOnce);
const [client] = RSASignerFactoryMockExpectation.firstCall.args;
t.is(client, clientStub);
}
);

testProp(
"#address is set at construction",
[addressArbitrary],
(t, address) => {
const signer = new RSASHA256Signer(
pki.rsa.generateKeyPair(2048),
with0x(address)
);
t.is(signer.address, with0x(address));
}
);
testProp("#address is set at construction", [RSAEOA], (t, rsaEOA) => {
t.is(
rsaEOA.signer.address,
predictRSASignerAddress(rsaEOA.signer.rsaPublicKey)
);
});

testProp(
'#publicKey is the concatenation of "e" and "n"',
Expand Down
41 changes: 4 additions & 37 deletions src/rsa-sha256-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {
TypedDataDefinition,
TypedData,
concat,
Client,
toHex,
hashTypedData,
hashMessage,
isHex,
} from "viem";

import { getRSASignerFactory, with0x, factory, without0x } from "./utils";
import { predictRSASignerAddress, with0x, without0x } from "./utils";
import {
EthSafeSignature,
buildSignatureBytes,
Expand All @@ -28,26 +27,10 @@ import {
class RSASHA256Signer implements SmartAccountSigner {
public readonly source: SmartAccountSigner["source"] = "custom";
public readonly type: SmartAccountSigner["type"] = "local";
public readonly address: SmartAccountSigner["address"];

constructor(
private readonly keypair: pki.rsa.KeyPair,
public readonly address: SmartAccountSigner["address"]
) {}

static async from<TClient extends Client>(
keypair: pki.rsa.KeyPair,
viemClient: TClient
): Promise<RSASHA256Signer> {
const contract = getRSASignerFactory(viemClient);
const address = await contract.read.predictDeterministicAddress([
{
exponent: with0x(keypair.privateKey.e.toString(16)),
modulus: with0x(keypair.privateKey.n.toString(16)),
},
factory,
]);
const signer = new RSASHA256Signer(keypair, address);
return signer;
constructor(private readonly keypair: pki.rsa.KeyPair) {
this.address = predictRSASignerAddress(this.rsaPublicKey);
}

/**
Expand Down Expand Up @@ -101,22 +84,6 @@ class RSASHA256Signer implements SmartAccountSigner {
* @dev Adapter of RSASHA256Signer to support signing messages encoded in the format of a Safe{Wallet}.
*/
class RSASHA256SafeSigner extends RSASHA256Signer {
static async from<TClient extends Client>(
keypair: pki.rsa.KeyPair,
viemClient: TClient
): Promise<RSASHA256Signer> {
const contract = getRSASignerFactory(viemClient);
const address = await contract.read.predictDeterministicAddress([
{
exponent: with0x(keypair.privateKey.e.toString(16)),
modulus: with0x(keypair.privateKey.n.toString(16)),
},
factory,
]);
const signer = new RSASHA256SafeSigner(keypair, address);
return signer;
}

signMessage = async ({
message,
}: {
Expand Down
51 changes: 47 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { Address, Client, Hex, getContract } from "viem";
import RSASignerFactoryABI from "./abi/RSASignerFactory";
import {
Address,
Client,
Hex,
concatHex,
encodeAbiParameters,
getContract,
getContractAddress,
keccak256,
} from "viem";
import RSASignerFactoryABI from "./artifacts/RSASignerFactory";
import { pki } from "node-forge";

function with0x(str: string): Hex {
const without0xStr = without0x(str);
Expand All @@ -15,13 +25,46 @@ function without0x(str: string): string {

// Universal RSASigner factory contract address
export const factory: Address = "0xd6dA52A1Ad12618c7228920003EAF39f37F5d693";
export const implementation: Address =
"0x832641fC286F331D01d482151217F9D381a1f0f6"; // Deployed along with the factory

function getRSASignerFactory<TClient extends Client>(client: TClient) {
return getContract({
address: factory,
abi: RSASignerFactoryABI,
abi: RSASignerFactoryABI.abi,
client,
});
}

export { with0x, without0x, getRSASignerFactory };
function predictRSASignerAddress(publicKey: pki.rsa.PublicKey) {
const publicKeyAbi = [
{
type: "tuple",
components: [
{ name: "exponent", type: "bytes" },
{ name: "modulus", type: "bytes" },
],
},
] as const;
return getContractAddress({
// According to EIP-1167
bytecode: concatHex([
"0x3d602d80600a3d3981f3363d3d373d3d3d363d73",
implementation,
"0x5af43d82803e903d91602b57fd5bf3",
]),
from: factory,
opcode: "CREATE2",
salt: keccak256(
encodeAbiParameters(publicKeyAbi, [
{
exponent: with0x(publicKey.e.toString(16)),
modulus: with0x(publicKey.n.toString(16)),
},
]),
"bytes"
),
});
}

export { with0x, without0x, getRSASignerFactory, predictRSASignerAddress };

0 comments on commit 2c82862

Please sign in to comment.