Skip to content

Commit

Permalink
Refactor/identity keys persistence and signing (#154)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
devceline committed Dec 18, 2023
1 parent 1705bc8 commit 458c711
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 81 deletions.
18 changes: 18 additions & 0 deletions misc/identity-keys/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# @walletconnect/identity-keys

## 1.0.1

### Patch Changes

- Replace viem with @ethersproject/transactions and @ethersproject/hash to optimize for size

## 1.0.0

### Major Changes

- Refactor registration to be multi function, with the following flow:

- `prepareRegistration`,
- sign `message` independently,
- then pass that `signature` along with `registerParams` from `prepareRegistration` into register.

- Removes `onSign` function, instead passing signature to the second part of a duo function operation.

## 0.2.3

### Patch Changes
Expand Down
4 changes: 3 additions & 1 deletion misc/identity-keys/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@walletconnect/identity-keys",
"version": "0.2.3",
"version": "1.0.1",
"description": "Utilities to register, resolve and unregister identity keys",
"keywords": [
"identity",
Expand Down Expand Up @@ -51,6 +51,8 @@
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@ethersproject/hash": "^5.7.0",
"@ethersproject/transactions": "^5.7.0",
"@noble/ed25519": "^1.7.1",
"@walletconnect/cacao": "1.0.2",
"@walletconnect/core": "^2.10.1",
Expand Down
150 changes: 91 additions & 59 deletions misc/identity-keys/src/identity-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
generateJWT,
jwtExp,
} from "@walletconnect/did-jwt";
import { hashMessage } from "@ethersproject/hash";
import { recoverAddress } from "@ethersproject/transactions";
import { ICore, IStore } from "@walletconnect/types";
import { formatMessage, generateRandomBytes32 } from "@walletconnect/utils";
import axios from "axios";
Expand Down Expand Up @@ -42,94 +44,101 @@ export class IdentityKeys implements IIdentityKeys {
await this.identityKeys.init();
};

private generateIdentityKey = async (accountId: string) => {
const privateKey = ed25519.utils.randomPrivateKey();
const publicKey = await ed25519.getPublicKey(privateKey);

const pubKeyHex = ed25519.utils.bytesToHex(publicKey).toLowerCase();
const privKeyHex = ed25519.utils.bytesToHex(privateKey).toLowerCase();

return {
pubKeyHex,
persist: async () => {
// Deferring persistence to caller to only persist after success
// of signing and registering full cacao on keyserver
await this.core.crypto.keychain.set(pubKeyHex, privKeyHex);
await this.identityKeys.set(accountId, {
identityKeyPriv: privKeyHex,
identityKeyPub: pubKeyHex,
accountId,
});
},
};
};

public generateIdAuth = async (accountId: string, payload: JwtPayload) => {
const { identityKeyPub, identityKeyPriv } = this.identityKeys.get(accountId);

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

public async registerIdentity({
accountId,
onSign,
public isRegistered(account: string) {
return this.identityKeys.keys.includes(account);
}

public async prepareRegistration({
domain,
accountId,
statement,
}: {
domain: string;
statement?: string;
accountId: string;
}) {
const { privateKey, pubKeyHex } = await this.generateIdentityKey();

const cacaoPayload = {
aud: encodeEd25519Key(pubKeyHex),
statement,
domain,
iss: composeDidPkh(accountId),
nonce: generateRandomBytes32(),
iat: new Date().toISOString(),
version: "1",
resources: [this.keyserverUrl],
};

return {
message: formatMessage(cacaoPayload, composeDidPkh(accountId)),
registerParams: {
cacaoPayload,
privateIdentityKey: privateKey,
},
};
}

public async registerIdentity({
registerParams,
signature,
}: RegisterIdentityParams): Promise<string> {
if (this.identityKeys.keys.includes(accountId)) {
const accountId = registerParams.cacaoPayload.iss.split(":").slice(-3).join(":");

if (this.isRegistered(accountId)) {
const storedKeyPair = this.identityKeys.get(accountId);
return storedKeyPair.identityKeyPub;
} else {
try {
const { pubKeyHex, persist } = await this.generateIdentityKey(accountId);
const message = formatMessage(registerParams.cacaoPayload, registerParams.cacaoPayload.iss);

if (!signature) {
throw new Error(`Provided an invalid signature. Expected a string but got: ${signature}`);
}

const didKey = encodeEd25519Key(pubKeyHex);
const recoveredAddress = recoverAddress(hashMessage(message), signature);
const signatureValid =
recoveredAddress.toLowerCase() === accountId.split(":").pop()!.toLowerCase();

Check warning on line 107 in misc/identity-keys/src/identity-keys.ts

View workflow job for this annotation

GitHub Actions / CI (misc/identity-keys)

Forbidden non-null assertion

if (!signatureValid) {
throw new Error(`Provided an invalid signature. Signature ${signature} by account
${accountId} is not a valid signature for message ${message}`);
}

const url = `${this.keyserverUrl}/identity`;

const cacao: Cacao = {
h: {
t: "eip4361",
},
p: {
aud: didKey,
statement,
domain,
iss: composeDidPkh(accountId),
nonce: generateRandomBytes32(),
iat: new Date().toISOString(),
version: "1",
resources: [this.keyserverUrl],
},
p: registerParams.cacaoPayload,
s: {
t: "eip191",
s: "",
s: signature,
},
};

const cacaoMessage = formatMessage(cacao.p, composeDidPkh(accountId));

const signature = await onSign(cacaoMessage);

if (!signature) {
throw new Error(`Provided an invalid signature. Expected a string but got: ${signature}`);
}

const url = `${this.keyserverUrl}/identity`;

try {
await axios.post(url, {
cacao: {
...cacao,
s: {
...cacao.s,
s: signature,
},
},
});
await axios.post(url, { cacao });
} catch (e) {
throw new Error(`Failed to register on keyserver: ${e}`);
}

await persist();
// Persist keys only after successful registration
const { pubKeyHex, privKeyHex } = await this.getKeyData(registerParams.privateIdentityKey);

await this.core.crypto.keychain.set(pubKeyHex, privKeyHex);
await this.identityKeys.set(accountId, {
identityKeyPriv: privKeyHex,
identityKeyPub: pubKeyHex,
accountId,
});

return pubKeyHex;
} catch (error) {
Expand Down Expand Up @@ -197,4 +206,27 @@ export class IdentityKeys implements IIdentityKeys {
public async hasIdentity({ account }: GetIdentityParams): Promise<boolean> {
return this.identityKeys.keys.includes(account);
}

// --------------------------- Private Helpers -----------------------------//

private generateIdentityKey = () => {
const privateKey = ed25519.utils.randomPrivateKey();

return this.getKeyData(privateKey);
};

private getKeyHex = (key: Uint8Array) => {
return ed25519.utils.bytesToHex(key).toLowerCase();
};

private getKeyData = async (privateKey: Uint8Array) => {
const publicKey = await ed25519.getPublicKey(privateKey);

return {
publicKey,
privateKey,
pubKeyHex: this.getKeyHex(publicKey),
privKeyHex: this.getKeyHex(privateKey),
};
};
}
11 changes: 6 additions & 5 deletions misc/identity-keys/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Cacao } from "@walletconnect/cacao";
import { CacaoPayload, Cacao } from "@walletconnect/cacao";
import { JwtPayload } from "@walletconnect/did-jwt";

export interface RegisterIdentityParams {
accountId: string;
onSign: (message: string) => Promise<string | undefined>;
domain: string;
statement: string;
registerParams: {
cacaoPayload: CacaoPayload;
privateIdentityKey: Uint8Array;
};
signature: string;
}

export interface ResolveIdentityParams {
Expand Down
44 changes: 29 additions & 15 deletions misc/identity-keys/test/identity-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ describe("@walletconnect/identity-keys", () => {

let wallet: Wallet;
let accountId: string;
let onSign: (m: string) => Promise<string>;
let core: ICore;
let identityKeys: IdentityKeys;

beforeEach(async () => {
wallet = Wallet.createRandom();
accountId = `eip155:1:${wallet.address}`;
onSign = (m) => wallet.signMessage(m);

core = new Core({ projectId: PROJECT_ID });

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

it("registers on keyserver", async () => {
const identity = await identityKeys.registerIdentity({
const { message, registerParams } = await identityKeys.prepareRegistration({
accountId,
statement,
onSign,
domain,
});

const signature = await wallet.signMessage(message);

const identity = await identityKeys.registerIdentity({
registerParams,
signature,
});

const encodedIdentity = encodeEd25519Key(identity).split(":")[2];

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

it("does not persist identity keys that failed to register", async () => {
const { registerParams } = await identityKeys.prepareRegistration({
accountId,
statement,
domain,
});

// rejectedWith & rejected are not supported on this version of chai
let failMessage = "";

const signature = await wallet.signMessage("otherMessage");
await identityKeys
.registerIdentity({
accountId,
statement,
onSign: () => Promise.resolve("badSignature"),
domain,
registerParams,
signature,
})
.catch((err) => (failMessage = err.message));

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

const keys = identityKeys.identityKeys.getAll();
expect(keys.length).eq(0);
});

it("prevents registering with empty signatures", async () => {
const { registerParams } = await identityKeys.prepareRegistration({
accountId,
statement,
domain,
});

// rejectedWith & rejected are not supported on this version of chai
let failMessage = "";
await identityKeys
.registerIdentity({
accountId,
statement,
onSign: () => Promise.resolve(""),
domain,
registerParams,
signature: "",
})
.catch((err) => (failMessage = err.message));

Expand Down
1 change: 1 addition & 0 deletions misc/identity-keys/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist/types",
"ignoreDeprecations": "5.0",
"emitDeclarationOnly": true
}
}
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 458c711

Please sign in to comment.