Skip to content

Commit

Permalink
[sdk] Add new options and improve docs for verifying signatures are v… (
Browse files Browse the repository at this point in the history
#20664)

…alid for a specific address

## Description 

Describe the changes or additions included in this PR.

## Test plan 

How did you test the new or updated feature?

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
- [ ] REST API:
  • Loading branch information
hayes-mysten authored Dec 19, 2024
1 parent dc0e21e commit 85bd9e4
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 24 deletions.
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 {
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/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

0 comments on commit 85bd9e4

Please sign in to comment.