Skip to content

Commit 458c711

Browse files
authored
Refactor/identity keys persistence and signing (#154)
* chore: refactor signature logic by adding util functions per step * chore: refactor persistence logic * test: finalize unit test refactor * chore: run prettier * chore: run lint * chore: ignore ts5.0 deprecate * chore: use utils sub module * chore: bump major version * Revert "chore: bump major version" This reverts commit 993e90f. * chore(release): bump major version using changeset * chore: update changelog * chore: replace viem with ethersproject * chore: bump version using changeset * chore: run prettier
1 parent 1705bc8 commit 458c711

File tree

7 files changed

+153
-81
lines changed

7 files changed

+153
-81
lines changed

misc/identity-keys/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# @walletconnect/identity-keys
22

3+
## 1.0.1
4+
5+
### Patch Changes
6+
7+
- Replace viem with @ethersproject/transactions and @ethersproject/hash to optimize for size
8+
9+
## 1.0.0
10+
11+
### Major Changes
12+
13+
- Refactor registration to be multi function, with the following flow:
14+
15+
- `prepareRegistration`,
16+
- sign `message` independently,
17+
- then pass that `signature` along with `registerParams` from `prepareRegistration` into register.
18+
19+
- Removes `onSign` function, instead passing signature to the second part of a duo function operation.
20+
321
## 0.2.3
422

523
### Patch Changes

misc/identity-keys/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@walletconnect/identity-keys",
3-
"version": "0.2.3",
3+
"version": "1.0.1",
44
"description": "Utilities to register, resolve and unregister identity keys",
55
"keywords": [
66
"identity",
@@ -51,6 +51,8 @@
5151
"webpack-cli": "^5.0.1"
5252
},
5353
"dependencies": {
54+
"@ethersproject/hash": "^5.7.0",
55+
"@ethersproject/transactions": "^5.7.0",
5456
"@noble/ed25519": "^1.7.1",
5557
"@walletconnect/cacao": "1.0.2",
5658
"@walletconnect/core": "^2.10.1",

misc/identity-keys/src/identity-keys.ts

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
generateJWT,
99
jwtExp,
1010
} from "@walletconnect/did-jwt";
11+
import { hashMessage } from "@ethersproject/hash";
12+
import { recoverAddress } from "@ethersproject/transactions";
1113
import { ICore, IStore } from "@walletconnect/types";
1214
import { formatMessage, generateRandomBytes32 } from "@walletconnect/utils";
1315
import axios from "axios";
@@ -42,94 +44,101 @@ export class IdentityKeys implements IIdentityKeys {
4244
await this.identityKeys.init();
4345
};
4446

45-
private generateIdentityKey = async (accountId: string) => {
46-
const privateKey = ed25519.utils.randomPrivateKey();
47-
const publicKey = await ed25519.getPublicKey(privateKey);
48-
49-
const pubKeyHex = ed25519.utils.bytesToHex(publicKey).toLowerCase();
50-
const privKeyHex = ed25519.utils.bytesToHex(privateKey).toLowerCase();
51-
52-
return {
53-
pubKeyHex,
54-
persist: async () => {
55-
// Deferring persistence to caller to only persist after success
56-
// of signing and registering full cacao on keyserver
57-
await this.core.crypto.keychain.set(pubKeyHex, privKeyHex);
58-
await this.identityKeys.set(accountId, {
59-
identityKeyPriv: privKeyHex,
60-
identityKeyPub: pubKeyHex,
61-
accountId,
62-
});
63-
},
64-
};
65-
};
66-
6747
public generateIdAuth = async (accountId: string, payload: JwtPayload) => {
6848
const { identityKeyPub, identityKeyPriv } = this.identityKeys.get(accountId);
6949

7050
return generateJWT([identityKeyPub, identityKeyPriv], payload);
7151
};
7252

73-
public async registerIdentity({
74-
accountId,
75-
onSign,
53+
public isRegistered(account: string) {
54+
return this.identityKeys.keys.includes(account);
55+
}
56+
57+
public async prepareRegistration({
7658
domain,
59+
accountId,
7760
statement,
61+
}: {
62+
domain: string;
63+
statement?: string;
64+
accountId: string;
65+
}) {
66+
const { privateKey, pubKeyHex } = await this.generateIdentityKey();
67+
68+
const cacaoPayload = {
69+
aud: encodeEd25519Key(pubKeyHex),
70+
statement,
71+
domain,
72+
iss: composeDidPkh(accountId),
73+
nonce: generateRandomBytes32(),
74+
iat: new Date().toISOString(),
75+
version: "1",
76+
resources: [this.keyserverUrl],
77+
};
78+
79+
return {
80+
message: formatMessage(cacaoPayload, composeDidPkh(accountId)),
81+
registerParams: {
82+
cacaoPayload,
83+
privateIdentityKey: privateKey,
84+
},
85+
};
86+
}
87+
88+
public async registerIdentity({
89+
registerParams,
90+
signature,
7891
}: RegisterIdentityParams): Promise<string> {
79-
if (this.identityKeys.keys.includes(accountId)) {
92+
const accountId = registerParams.cacaoPayload.iss.split(":").slice(-3).join(":");
93+
94+
if (this.isRegistered(accountId)) {
8095
const storedKeyPair = this.identityKeys.get(accountId);
8196
return storedKeyPair.identityKeyPub;
8297
} else {
8398
try {
84-
const { pubKeyHex, persist } = await this.generateIdentityKey(accountId);
99+
const message = formatMessage(registerParams.cacaoPayload, registerParams.cacaoPayload.iss);
100+
101+
if (!signature) {
102+
throw new Error(`Provided an invalid signature. Expected a string but got: ${signature}`);
103+
}
85104

86-
const didKey = encodeEd25519Key(pubKeyHex);
105+
const recoveredAddress = recoverAddress(hashMessage(message), signature);
106+
const signatureValid =
107+
recoveredAddress.toLowerCase() === accountId.split(":").pop()!.toLowerCase();
108+
109+
if (!signatureValid) {
110+
throw new Error(`Provided an invalid signature. Signature ${signature} by account
111+
${accountId} is not a valid signature for message ${message}`);
112+
}
113+
114+
const url = `${this.keyserverUrl}/identity`;
87115

88116
const cacao: Cacao = {
89117
h: {
90118
t: "eip4361",
91119
},
92-
p: {
93-
aud: didKey,
94-
statement,
95-
domain,
96-
iss: composeDidPkh(accountId),
97-
nonce: generateRandomBytes32(),
98-
iat: new Date().toISOString(),
99-
version: "1",
100-
resources: [this.keyserverUrl],
101-
},
120+
p: registerParams.cacaoPayload,
102121
s: {
103122
t: "eip191",
104-
s: "",
123+
s: signature,
105124
},
106125
};
107126

108-
const cacaoMessage = formatMessage(cacao.p, composeDidPkh(accountId));
109-
110-
const signature = await onSign(cacaoMessage);
111-
112-
if (!signature) {
113-
throw new Error(`Provided an invalid signature. Expected a string but got: ${signature}`);
114-
}
115-
116-
const url = `${this.keyserverUrl}/identity`;
117-
118127
try {
119-
await axios.post(url, {
120-
cacao: {
121-
...cacao,
122-
s: {
123-
...cacao.s,
124-
s: signature,
125-
},
126-
},
127-
});
128+
await axios.post(url, { cacao });
128129
} catch (e) {
129130
throw new Error(`Failed to register on keyserver: ${e}`);
130131
}
131132

132-
await persist();
133+
// Persist keys only after successful registration
134+
const { pubKeyHex, privKeyHex } = await this.getKeyData(registerParams.privateIdentityKey);
135+
136+
await this.core.crypto.keychain.set(pubKeyHex, privKeyHex);
137+
await this.identityKeys.set(accountId, {
138+
identityKeyPriv: privKeyHex,
139+
identityKeyPub: pubKeyHex,
140+
accountId,
141+
});
133142

134143
return pubKeyHex;
135144
} catch (error) {
@@ -197,4 +206,27 @@ export class IdentityKeys implements IIdentityKeys {
197206
public async hasIdentity({ account }: GetIdentityParams): Promise<boolean> {
198207
return this.identityKeys.keys.includes(account);
199208
}
209+
210+
// --------------------------- Private Helpers -----------------------------//
211+
212+
private generateIdentityKey = () => {
213+
const privateKey = ed25519.utils.randomPrivateKey();
214+
215+
return this.getKeyData(privateKey);
216+
};
217+
218+
private getKeyHex = (key: Uint8Array) => {
219+
return ed25519.utils.bytesToHex(key).toLowerCase();
220+
};
221+
222+
private getKeyData = async (privateKey: Uint8Array) => {
223+
const publicKey = await ed25519.getPublicKey(privateKey);
224+
225+
return {
226+
publicKey,
227+
privateKey,
228+
pubKeyHex: this.getKeyHex(publicKey),
229+
privKeyHex: this.getKeyHex(privateKey),
230+
};
231+
};
200232
}

misc/identity-keys/src/types.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Cacao } from "@walletconnect/cacao";
1+
import { CacaoPayload, Cacao } from "@walletconnect/cacao";
22
import { JwtPayload } from "@walletconnect/did-jwt";
33

44
export interface RegisterIdentityParams {
5-
accountId: string;
6-
onSign: (message: string) => Promise<string | undefined>;
7-
domain: string;
8-
statement: string;
5+
registerParams: {
6+
cacaoPayload: CacaoPayload;
7+
privateIdentityKey: Uint8Array;
8+
};
9+
signature: string;
910
}
1011

1112
export interface ResolveIdentityParams {

misc/identity-keys/test/identity-keys.test.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@ describe("@walletconnect/identity-keys", () => {
1515

1616
let wallet: Wallet;
1717
let accountId: string;
18-
let onSign: (m: string) => Promise<string>;
1918
let core: ICore;
2019
let identityKeys: IdentityKeys;
2120

2221
beforeEach(async () => {
2322
wallet = Wallet.createRandom();
2423
accountId = `eip155:1:${wallet.address}`;
25-
onSign = (m) => wallet.signMessage(m);
26-
2724
core = new Core({ projectId: PROJECT_ID });
2825

2926
identityKeys = identityKeys = new IdentityKeys(core, DEFAULT_KEYSERVER_URL);
@@ -33,13 +30,19 @@ describe("@walletconnect/identity-keys", () => {
3330
});
3431

3532
it("registers on keyserver", async () => {
36-
const identity = await identityKeys.registerIdentity({
33+
const { message, registerParams } = await identityKeys.prepareRegistration({
3734
accountId,
3835
statement,
39-
onSign,
4036
domain,
4137
});
4238

39+
const signature = await wallet.signMessage(message);
40+
41+
const identity = await identityKeys.registerIdentity({
42+
registerParams,
43+
signature,
44+
});
45+
4346
const encodedIdentity = encodeEd25519Key(identity).split(":")[2];
4447

4548
const fetchUrl = `${DEFAULT_KEYSERVER_URL}/identity?publicKey=${encodedIdentity}`;
@@ -50,34 +53,45 @@ describe("@walletconnect/identity-keys", () => {
5053
});
5154

5255
it("does not persist identity keys that failed to register", async () => {
56+
const { registerParams } = await identityKeys.prepareRegistration({
57+
accountId,
58+
statement,
59+
domain,
60+
});
61+
5362
// rejectedWith & rejected are not supported on this version of chai
5463
let failMessage = "";
64+
65+
const signature = await wallet.signMessage("otherMessage");
5566
await identityKeys
5667
.registerIdentity({
57-
accountId,
58-
statement,
59-
onSign: () => Promise.resolve("badSignature"),
60-
domain,
68+
registerParams,
69+
signature,
6170
})
6271
.catch((err) => (failMessage = err.message));
6372

64-
expect(failMessage).eq(
65-
`Failed to register on keyserver: AxiosError: Request failed with status code 400`,
73+
expect(failMessage).match(
74+
new RegExp(`Provided an invalid signature. Signature ${signature} by account
75+
${accountId} is not a valid signature for message .*`),
6676
);
6777

6878
const keys = identityKeys.identityKeys.getAll();
6979
expect(keys.length).eq(0);
7080
});
7181

7282
it("prevents registering with empty signatures", async () => {
83+
const { registerParams } = await identityKeys.prepareRegistration({
84+
accountId,
85+
statement,
86+
domain,
87+
});
88+
7389
// rejectedWith & rejected are not supported on this version of chai
7490
let failMessage = "";
7591
await identityKeys
7692
.registerIdentity({
77-
accountId,
78-
statement,
79-
onSign: () => Promise.resolve(""),
80-
domain,
93+
registerParams,
94+
signature: "",
8195
})
8296
.catch((err) => (failMessage = err.message));
8397

misc/identity-keys/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"compilerOptions": {
55
"rootDir": "src",
66
"outDir": "./dist/types",
7+
"ignoreDeprecations": "5.0",
78
"emitDeclarationOnly": true
89
}
910
}

package-lock.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)