Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sdk] Add new options and improve docs for verifying signatures are v… #20664

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-apples-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui': minor
---

Add a new `address` options on methods that verify signatures that ensures the signature is valid for the provided address
5 changes: 5 additions & 0 deletions .changeset/small-toys-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui': minor
---

Add a new `publicKey.verifyAddress` method on PublicKey instances
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
60 changes: 51 additions & 9 deletions sdk/docs/pages/typescript/cryptography/keypairs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,29 @@ const { signature } = await keypair.signPersonalMessage(message);

const publicKey = await verifyPersonalMessageSignature(message, signature);

if (publicKey.toSuiAddress() !== keypair.getPublicKey().toSuiAddress()) {
if (publicKey.verifyAddress(keypair.getPublicKey().toSuiAddress())) {
throw new Error('Signature was valid, but was signed by a different key pair');
}
```

## Verifying that a signature is valid for a specific address

`verifyPersonalMessageSignature` and `verifyTransactionSignature` accept an optional `address`, and
will throw an error if the signature is not valid for the provided address.

```typescript
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { verifyPersonalMessageSignature } from '@mysten/sui/verify';

const keypair = new Ed25519Keypair();
const message = new TextEncoder().encode('hello world');
const { signature } = await keypair.signPersonalMessage(message);

await verifyPersonalMessageSignature(message, signature, {
address: keypair.getPublicKey().toSuiAddress(),
});
```

## Verifying transaction signatures

Verifying transaction signatures is similar to personal message signature verification, except you
Expand All @@ -108,14 +126,10 @@ const bytes = await tx.build({ client });
const keypair = new Ed25519Keypair();
const { signature } = await keypair.signTransaction(bytes);

// if you have a public key, you can verify it
// const isValid = await publicKey.verifyTransaction(bytes, signature);
// or get the public key from the transaction
const publicKey = await verifyTransactionSignature(bytes, signature);

if (publicKey.toSuiAddress() !== keypair.getPublicKey().toSuiAddress()) {
throw new Error('Signature was valid, but was signed by a different keyPair');
}
await verifyTransactionSignature(bytes, signature, {
// optionally verify that the signature is valid for a specific address
address: keypair.getPublicKey().toSuiAddress(),
});
```

## Verifying zkLogin signatures
Expand All @@ -137,6 +151,34 @@ const publicKey = await verifyPersonalMessageSignature(message, zkSignature, {
});
```

For some zklogin accounts, there are 2 valid addresses for a given set of inputs. This means you may
run into issues if you try to compare the address returned by `publicKey.toSuiAddress()` directly
with an expected address.

Instead, you can either pass in the expected address during verification, or use the
`publicKey.verifyAddress(address)` method:

```typescript
import { SuiGraphQLClient } from '@mysten/sui/graphql';
import { verifyPersonalMessageSignature } from '@mysten/sui/verify';

const publicKey = await verifyPersonalMessageSignature(message, zkSignature, {
client: new SuiGraphQLClient({
url: 'https://sui-testnet.mystenlabs.com/graphql',
}),
// Pass in the expected address, and the verification method will throw an error if the signature is not valid for the provided address
address: '0x...expectedAddress',
});
// or

if (!publicKey.verifyAddress('0x...expectedAddress')) {
throw new Error('Signature was valid, but was signed by a different key pair');
}
```

Both of these methods will check the signature against both the standard and
[legacy versions of the zklogin address](https://sdk.mystenlabs.com/typescript/zklogin#legacy-addresses).

## Deriving a key pair from a mnemonic

The Sui TypeScript SDK supports deriving a key pair from a mnemonic phrase. This can be useful when
Expand Down
7 changes: 7 additions & 0 deletions sdk/typescript/src/cryptography/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ export abstract class PublicKey {
return this.verifyWithIntent(transaction, signature, 'TransactionData');
}

/**
* Verifies that the public key is associated with the provided address
*/
verifyAddress(address: string): boolean {
return this.toSuiAddress() === address;
}

/**
* Returns the bytes representation of the public key
* prefixed with the signature scheme flag
Expand Down
24 changes: 21 additions & 3 deletions sdk/typescript/src/verify/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,30 @@ import { Secp256r1PublicKey } from '../keypairs/secp256r1/publickey.js';
import { MultiSigPublicKey } from '../multisig/publickey.js';
import { ZkLoginPublicIdentifier } from '../zklogin/publickey.js';

export async function verifySignature(bytes: Uint8Array, signature: string): Promise<PublicKey> {
export async function verifySignature(
bytes: Uint8Array,
signature: string,
options?: {
address?: string;
},
): Promise<PublicKey> {
const parsedSignature = parseSignature(signature);

if (!(await parsedSignature.publicKey.verify(bytes, parsedSignature.serializedSignature))) {
throw new Error(`Signature is not valid for the provided data`);
}

if (options?.address && !parsedSignature.publicKey.verifyAddress(options.address)) {
throw new Error(`Signature is not valid for the provided address`);
}

return parsedSignature.publicKey;
}

export async function verifyPersonalMessageSignature(
message: Uint8Array,
signature: string,
options: { client?: SuiGraphQLClient } = {},
options: { client?: SuiGraphQLClient; address?: string } = {},
): Promise<PublicKey> {
const parsedSignature = parseSignature(signature, options);

Expand All @@ -40,13 +50,17 @@ export async function verifyPersonalMessageSignature(
throw new Error(`Signature is not valid for the provided message`);
}

if (options?.address && !parsedSignature.publicKey.verifyAddress(options.address)) {
throw new Error(`Signature is not valid for the provided address`);
}

return parsedSignature.publicKey;
}

export async function verifyTransactionSignature(
transaction: Uint8Array,
signature: string,
options: { client?: SuiGraphQLClient } = {},
options: { client?: SuiGraphQLClient; address?: string } = {},
): Promise<PublicKey> {
const parsedSignature = parseSignature(signature, options);

Expand All @@ -59,6 +73,10 @@ export async function verifyTransactionSignature(
throw new Error(`Signature is not valid for the provided Transaction`);
}

if (options?.address && !parsedSignature.publicKey.verifyAddress(options.address)) {
throw new Error(`Signature is not valid for the provided address`);
}

return parsedSignature.publicKey;
}

Expand Down
25 changes: 18 additions & 7 deletions sdk/typescript/src/zklogin/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,22 @@ export class ZkLoginPublicIdentifier extends PublicKey {

override toSuiAddress(): string {
if (this.#legacyAddress) {
const legacyBytes = normalizeZkLoginPublicKeyBytes(this.#data, true);
const addressBytes = new Uint8Array(legacyBytes.length + 1);
addressBytes[0] = this.flag();
addressBytes.set(legacyBytes, 1);
return normalizeSuiAddress(
bytesToHex(blake2b(addressBytes, { dkLen: 32 })).slice(0, SUI_ADDRESS_LENGTH * 2),
);
return this.#toLegacyAddress();
}

return super.toSuiAddress();
}

#toLegacyAddress() {
const legacyBytes = normalizeZkLoginPublicKeyBytes(this.#data, true);
const addressBytes = new Uint8Array(legacyBytes.length + 1);
addressBytes[0] = this.flag();
addressBytes.set(legacyBytes, 1);
return normalizeSuiAddress(
bytesToHex(blake2b(addressBytes, { dkLen: 32 })).slice(0, SUI_ADDRESS_LENGTH * 2),
);
}

/**
* Return the byte array representation of the zkLogin public identifier
*/
Expand Down Expand Up @@ -118,6 +122,13 @@ export class ZkLoginPublicIdentifier extends PublicKey {
client: this.#client,
});
}

/**
* Verifies that the public key is associated with the provided address
*/
override verifyAddress(address: string): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever way to do this

return address === super.toSuiAddress() || address === this.#toLegacyAddress();
}
}

// Derive the public identifier for zklogin based on address seed and iss.
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/test/e2e/data/coin_metadata/Move.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
2 changes: 1 addition & 1 deletion sdk/typescript/test/e2e/data/dynamic_fields/Move.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
2 changes: 1 addition & 1 deletion sdk/typescript/test/e2e/data/id_entry_args/Move.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
2 changes: 1 addition & 1 deletion sdk/typescript/test/e2e/data/serializer/Move.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ dependencies = [
]

[move.toolchain-version]
compiler-version = "1.38.0"
compiler-version = "1.40.0"
edition = "2024.beta"
flavor = "sui"
Loading
Loading