Skip to content

Commit

Permalink
sdk: fix personal message to use bcs (MystenLabs#12997)
Browse files Browse the repository at this point in the history
## Description 

fix a bug that the signMessage in rust vs typescript has a discrepancy.
this is make typescript use bcs serialization for a vector instead of
just a plain vector.

```
PersonalMessage {
  message: Vec<u8>
}
```
## Test Plan 

existing tests in ts still passes (bc verify and sign are both changed
to the new format).
tested against rust that a ts produced sig now verifies in rust. 

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes

---------

Co-authored-by: Michael Hayes <michael.hayes@mystenlabs.com>
  • Loading branch information
joyqvq and hayes-mysten authored Jul 19, 2023
1 parent 007a42c commit 09f4ed3
Show file tree
Hide file tree
Showing 18 changed files with 397 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-pugs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui.js': minor
---

signMesssage uses BCS serialization for vector
5 changes: 5 additions & 0 deletions dapps/kiosk-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ A simple CLI application written in NodeJS to showcase Kiosk usage and basic app
## Install and use

Export a mnemonic phrase

```
export MNEMONIC="..."
```

Create a Kiosk

```
pnpm cli new
```

Mint a TestItem

```
pnpm cli mint-to-kiosk
```

View Kiosk contents

```
pnpm cli contents
Expand All @@ -37,6 +41,7 @@ pnpm cli contents
```

List an item in the Kiosk

```
# list an item in the Kiosk
pnpm list <item_id> <price>
Expand Down
1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ packages:
- '!sdk/typescript/faucet'
- '!sdk/typescript/transactions'
- '!sdk/typescript/client'
- '!sdk/typescript/verify'
8 changes: 7 additions & 1 deletion sdk/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"faucet",
"keypairs",
"src",
"transactions"
"transactions",
"verify"
],
"engines": {
"node": ">=16"
Expand Down Expand Up @@ -67,6 +68,11 @@
"source": "./src/builder/export.ts",
"import": "./dist/esm/builder/export.js",
"require": "./dist/cjs/builder/export.js"
},
"./verify": {
"source": "./src/verify/index.ts",
"import": "./dist/esm/verify/index.js",
"require": "./dist/cjs/verify/index.js"
}
},
"scripts": {
Expand Down
15 changes: 13 additions & 2 deletions sdk/typescript/src/cryptography/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toSerializedSignature } from './signature.js';
import type { SignatureScheme } from './signature.js';
import { IntentScope, messageWithIntent } from './intent.js';
import { blake2b } from '@noble/hashes/blake2b';
import { bcs } from '../types/sui-bcs.js';

export const PRIVATE_KEY_SIZE = 32;
export const LEGACY_PRIVATE_KEY_SIZE = 64;
Expand Down Expand Up @@ -47,8 +48,18 @@ export abstract class BaseSigner {
return this.signWithIntent(bytes, IntentScope.TransactionData);
}

async signPersonalMessage(bytes: Uint8Array) {
return this.signWithIntent(
bcs.ser(['vector', 'u8'], bytes).toBytes(),
IntentScope.PersonalMessage,
);
}

/**
* @deprecated use `signPersonalMessage` instead
*/
async signMessage(bytes: Uint8Array) {
return this.signWithIntent(bytes, IntentScope.PersonalMessage);
return this.signPersonalMessage(bytes);
}

toSuiAddress(): string {
Expand All @@ -57,7 +68,7 @@ export abstract class BaseSigner {

/**
* Return the signature for the data.
* Prefer the async verion {@link sign}, as this method will be deprecated in a future release.
* Prefer the async version {@link sign}, as this method will be deprecated in a future release.
*/
abstract signData(data: Uint8Array): Uint8Array;

Expand Down
37 changes: 37 additions & 0 deletions sdk/typescript/src/cryptography/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { toB64 } from '@mysten/bcs';
import { IntentScope, messageWithIntent } from './intent.js';
import { blake2b } from '@noble/hashes/blake2b';
import { bcs } from '../types/sui-bcs.js';

/**
* Value to be converted into public key.
Expand Down Expand Up @@ -63,6 +66,35 @@ export abstract class PublicKey {
return toB64(suiPublicKey);
}

verifyWithIntent(
bytes: Uint8Array,
signature: Uint8Array,
intent: IntentScope,
): Promise<boolean> {
const intentMessage = messageWithIntent(intent, bytes);
const digest = blake2b(intentMessage, { dkLen: 32 });

return this.verify(digest, signature);
}

/**
* Verifies that the signature is valid for for the provided PersonalMessage
*/
verifyPersonalMessage(message: Uint8Array, signature: Uint8Array): Promise<boolean> {
return this.verifyWithIntent(
bcs.ser(['vector', 'u8'], message).toBytes(),
signature,
IntentScope.PersonalMessage,
);
}

/**
* Verifies that the signature is valid for for the provided TransactionBlock
*/
verifyTransactionBlock(transactionBlock: Uint8Array, signature: Uint8Array): Promise<boolean> {
return this.verifyWithIntent(transactionBlock, signature, IntentScope.TransactionData);
}

/**
* Return the byte array representation of the public key
*/
Expand All @@ -77,4 +109,9 @@ export abstract class PublicKey {
* Return signature scheme flag of the public key
*/
abstract flag(): number;

/**
* Verifies that the signature is valid for for the provided message
*/
abstract verify(data: Uint8Array, signature: Uint8Array): Promise<boolean>;
}
30 changes: 29 additions & 1 deletion sdk/typescript/src/cryptography/signature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { toB64 } from '@mysten/bcs';
import { fromB64, toB64 } from '@mysten/bcs';
import type { PublicKey } from './publickey.js';

export type SignatureScheme = 'ED25519' | 'Secp256k1' | 'Secp256r1' | 'MultiSig';
Expand Down Expand Up @@ -30,6 +30,12 @@ export const SIGNATURE_SCHEME_TO_FLAG = {
MultiSig: 0x03,
};

export const SIGNATURE_SCHEME_TO_SIZE = {
ED25519: 32,
Secp256k1: 33,
Secp256r1: 33,
};

export const SIGNATURE_FLAG_TO_SCHEME = {
0x00: 'ED25519',
0x01: 'Secp256k1',
Expand All @@ -51,3 +57,25 @@ export function toSerializedSignature({
serializedSignature.set(pubKeyBytes, 1 + signature.length);
return toB64(serializedSignature);
}

export function parseSerializedSignature(serializedSignature: SerializedSignature) {
const bytes = fromB64(serializedSignature);

const signatureScheme =
SIGNATURE_FLAG_TO_SCHEME[bytes[0] as keyof typeof SIGNATURE_FLAG_TO_SCHEME];

if (!(signatureScheme in SIGNATURE_SCHEME_TO_SIZE)) {
throw new Error('Unsupported signature scheme');
}

const size = SIGNATURE_SCHEME_TO_SIZE[signatureScheme as keyof typeof SIGNATURE_SCHEME_TO_SIZE];

const signature = bytes.slice(1, bytes.length - size);
const publicKey = bytes.slice(1 + signature.length);

return {
signatureScheme,
signature,
publicKey,
};
}
8 changes: 8 additions & 0 deletions sdk/typescript/src/keypairs/ed25519/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PublicKey } from '../../cryptography/publickey.js';
import { SIGNATURE_SCHEME_TO_FLAG } from '../../cryptography/signature.js';
import { bytesToHex } from '@noble/hashes/utils';
import { SUI_ADDRESS_LENGTH, normalizeSuiAddress } from '../../utils/sui-types.js';
import nacl from 'tweetnacl';

const PUBLIC_KEY_SIZE = 32;

Expand Down Expand Up @@ -73,4 +74,11 @@ export class Ed25519PublicKey extends PublicKey {
flag(): number {
return SIGNATURE_SCHEME_TO_FLAG['ED25519'];
}

/**
* Verifies that the signature is valid for for the provided message
*/
async verify(message: Uint8Array, signature: Uint8Array): Promise<boolean> {
return nacl.sign.detached.verify(message, signature, this.toBytes());
}
}
13 changes: 13 additions & 0 deletions sdk/typescript/src/keypairs/secp256k1/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { PublicKey } from '../../cryptography/publickey.js';
import type { PublicKeyInitData } from '../../cryptography/publickey.js';
import { SIGNATURE_SCHEME_TO_FLAG } from '../../cryptography/signature.js';
import { SUI_ADDRESS_LENGTH, normalizeSuiAddress } from '../../utils/sui-types.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';

const SECP256K1_PUBLIC_KEY_SIZE = 33;

Expand Down Expand Up @@ -73,4 +75,15 @@ export class Secp256k1PublicKey extends PublicKey {
flag(): number {
return SIGNATURE_SCHEME_TO_FLAG['Secp256k1'];
}

/**
* Verifies that the signature is valid for for the provided message
*/
async verify(message: Uint8Array, signature: Uint8Array): Promise<boolean> {
return secp256k1.verify(
secp256k1.Signature.fromCompact(signature),
sha256(message),
this.toBytes(),
);
}
}
13 changes: 13 additions & 0 deletions sdk/typescript/src/keypairs/secp256r1/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { PublicKey } from '../../cryptography/publickey.js';
import type { PublicKeyInitData } from '../../cryptography/publickey.js';
import { SIGNATURE_SCHEME_TO_FLAG } from '../../cryptography/signature.js';
import { SUI_ADDRESS_LENGTH, normalizeSuiAddress } from '../../utils/sui-types.js';
import { sha256 } from '@noble/hashes/sha256';
import { secp256r1 } from '@noble/curves/p256';

const SECP256R1_PUBLIC_KEY_SIZE = 33;

Expand Down Expand Up @@ -73,4 +75,15 @@ export class Secp256r1PublicKey extends PublicKey {
flag(): number {
return SIGNATURE_SCHEME_TO_FLAG['Secp256r1'];
}

/**
* Verifies that the signature is valid for for the provided message
*/
async verify(message: Uint8Array, signature: Uint8Array): Promise<boolean> {
return secp256r1.verify(
secp256r1.Signature.fromCompact(signature),
sha256(message),
this.toBytes(),
);
}
}
6 changes: 5 additions & 1 deletion sdk/typescript/src/signers/signer-with-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IntentScope, messageWithIntent } from '../cryptography/intent.js';
import type { Signer } from './signer.js';
import type { SignedTransaction, SignedMessage } from './types.js';
import type { SuiClient } from '../client/index.js';
import { bcs } from '../types/sui-bcs.js';

///////////////////////////////
// Exported Abstracts
Expand Down Expand Up @@ -74,7 +75,10 @@ export abstract class SignerWithProvider implements Signer {
*/
async signMessage(input: { message: Uint8Array }): Promise<SignedMessage> {
const signature = await this.signData(
messageWithIntent(IntentScope.PersonalMessage, input.message),
messageWithIntent(
IntentScope.PersonalMessage,
bcs.ser(['vector', 'u8'], input.message).toBytes(),
),
);

return {
Expand Down
45 changes: 28 additions & 17 deletions sdk/typescript/src/utils/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { fromB64 } from '@mysten/bcs';
import nacl from 'tweetnacl';
import type { IntentScope } from '../cryptography/intent.js';
import { IntentScope } from '../cryptography/intent.js';
import { messageWithIntent } from '../cryptography/intent.js';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import type { SerializedSignature } from '../cryptography/signature.js';
import { blake2b } from '@noble/hashes/blake2b';
import { toSingleSignaturePubkeyPair } from '../cryptography/utils.js';
import { bcs } from '../types/sui-bcs.js';

// TODO: This might actually make sense to eventually move to the `Keypair` instances themselves, as
// it could allow the Sui.js to be tree-shaken a little better, possibly allowing keypairs that are
Expand All @@ -22,21 +20,34 @@ export async function verifyMessage(
scope: IntentScope,
) {
const signature = toSingleSignaturePubkeyPair(serializedSignature);

if (scope === IntentScope.PersonalMessage) {
const messageBytes = messageWithIntent(
scope,
bcs.ser(['vector', 'u8'], typeof message === 'string' ? fromB64(message) : message).toBytes(),
);

if (await signature.pubKey.verify(blake2b(messageBytes, { dkLen: 32 }), signature.signature)) {
return true;
}

// Fallback for backwards compatibility, old versions of the SDK
// did not properly wrap PersonalMessages in a PersonalMessage bcs Struct
const unwrappedMessageBytes = messageWithIntent(
scope,
typeof message === 'string' ? fromB64(message) : message,
);

return signature.pubKey.verify(
blake2b(unwrappedMessageBytes, { dkLen: 32 }),
signature.signature,
);
}

const messageBytes = messageWithIntent(
scope,
typeof message === 'string' ? fromB64(message) : message,
);
const digest = blake2b(messageBytes, { dkLen: 32 });
switch (signature.signatureScheme) {
case 'ED25519':
return nacl.sign.detached.verify(digest, signature.signature, signature.pubKey.toBytes());
case 'Secp256k1':
return secp256k1.verify(
secp256k1.Signature.fromCompact(signature.signature),
sha256(digest),
signature.pubKey.toBytes(),
);
default:
throw new Error(`Unknown signature scheme: "${signature.signatureScheme}"`);
}

return signature.pubKey.verify(blake2b(messageBytes, { dkLen: 32 }), signature.signature);
}
Loading

0 comments on commit 09f4ed3

Please sign in to comment.