diff --git a/crates/sui/src/keytool.rs b/crates/sui/src/keytool.rs index 1be7d1dab2733..68562e567840a 100644 --- a/crates/sui/src/keytool.rs +++ b/crates/sui/src/keytool.rs @@ -91,16 +91,16 @@ impl KeyToolCommand { } KeyToolCommand::List => { println!( - " {0: ^42} | {1: ^45} | {2: ^1}", - "Sui Address", "Public Key (Base64)", "Flag" + " {0: ^42} | {1: ^45} | {2: ^6}", + "Sui Address", "Public Key (Base64)", "Scheme" ); println!("{}", ["-"; 100].join("")); for pub_key in keystore.keys() { println!( - " {0: ^42} | {1: ^45} | {2: ^1}", + " {0: ^42} | {1: ^45} | {2: ^6}", Into::::into(&pub_key), Base64::encode(&pub_key), - pub_key.flag() + pub_key.scheme().to_string() ); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4b3900f6cb13..6a002ff69fe1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,8 @@ importers: specifiers: '@mysten/bcs': workspace:* '@mysten/sui-open-rpc': file:../../crates/sui-open-rpc + '@noble/hashes': ^1.1.2 + '@noble/secp256k1': ^1.6.3 '@size-limit/preset-small-lib': ^7.0.8 '@types/bn.js': ^5.1.0 '@types/lossless-json': ^1.0.1 @@ -149,6 +151,8 @@ importers: vitest: ^0.22.1 dependencies: '@mysten/bcs': link:bcs + '@noble/hashes': 1.1.2 + '@noble/secp256k1': 1.6.3 bn.js: 5.2.1 buffer: 6.0.3 cross-fetch: 3.1.5 @@ -3152,6 +3156,14 @@ packages: dev: false optional: true + /@noble/hashes/1.1.2: + resolution: {integrity: sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==} + dev: false + + /@noble/secp256k1/1.6.3: + resolution: {integrity: sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==} + dev: false + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index c68a20f1df38a..02cd8a3689681 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -41,6 +41,13 @@ You can view the generated [Type Doc](https://typedoc.org/) for the [current rel For the latest docs for the `main` branch, run `pnpm doc` and open the [doc/index.html](doc/index.html) in your browser. +## Testing + +``` +cd sdk/typescript +pnpm run test +``` + ## Usage The `JsonRpcProvider` class provides a connection to the JSON-RPC Server and should be used for all read-only operations. The default URLs to connect with the RPC server are: @@ -76,8 +83,9 @@ To transfer a `0x2::coin::Coin`: ```typescript import { Ed25519Keypair, JsonRpcProvider, RawSigner } from '@mysten/sui.js'; -// Generate a new Keypair +// Generate a new Ed25519 Keypair const keypair = new Ed25519Keypair(); + const signer = new RawSigner( keypair, new JsonRpcProvider('https://gateway.devnet.sui.io:443') @@ -174,3 +182,17 @@ const publishTxn = await signer.publish( ); console.log('publishTxn', publishTxn); ``` + + +Alternatively, a Secp256k1 can be initiated: + +```typescript +import { Secp256k1Keypair, JsonRpcProvider, RawSigner } from '@mysten/sui.js'; +// Generate a new Secp256k1 Keypair +const keypair = new Secp256k1Keypair(); + +const signer = new RawSigner( + keypair, + new JsonRpcProvider('https://gateway.devnet.sui.io:443') +); +``` \ No newline at end of file diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index a05c5c6cbf0b6..59d85c447acd1 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -80,6 +80,8 @@ }, "dependencies": { "@mysten/bcs": "workspace:*", + "@noble/hashes": "^1.1.2", + "@noble/secp256k1": "^1.6.3", "bn.js": "^5.2.0", "buffer": "^6.0.3", "cross-fetch": "^3.1.5", diff --git a/sdk/typescript/src/cryptography/ed25519-keypair.ts b/sdk/typescript/src/cryptography/ed25519-keypair.ts index 10c285759fe1a..4aa36eef041ec 100644 --- a/sdk/typescript/src/cryptography/ed25519-keypair.ts +++ b/sdk/typescript/src/cryptography/ed25519-keypair.ts @@ -4,7 +4,8 @@ import nacl from 'tweetnacl'; import { Base64DataBuffer } from '../serialization/base64'; import { Keypair } from './keypair'; -import { PublicKey } from './publickey'; +import { Ed25519PublicKey } from './ed25519-publickey'; +import { SignatureScheme } from './publickey'; /** * Ed25519 Keypair data @@ -21,10 +22,10 @@ export class Ed25519Keypair implements Keypair { private keypair: Ed25519KeypairData; /** - * Create a new keypair instance. + * Create a new Ed25519 keypair instance. * Generate random keypair if no {@link Ed25519Keypair} is provided. * - * @param keypair ed25519 keypair + * @param keypair Ed25519 keypair */ constructor(keypair?: Ed25519KeypairData) { if (keypair) { @@ -33,16 +34,23 @@ export class Ed25519Keypair implements Keypair { this.keypair = nacl.sign.keyPair(); } } + + /** + * Get the key scheme of the keypair ED25519 + */ + getKeyScheme(): SignatureScheme { + return 'ED25519'; + } /** - * Generate a new random keypair + * Generate a new random Ed25519 keypair */ static generate(): Ed25519Keypair { return new Ed25519Keypair(nacl.sign.keyPair()); } /** - * Create a keypair from a raw secret key byte array. + * Create a Ed25519 keypair from a raw secret key byte array. * * This method should only be used to recreate a keypair from a previously * generated secret key. Generating keypairs from a random seed should be done @@ -70,7 +78,7 @@ export class Ed25519Keypair implements Keypair { } /** - * Generate a keypair from a 32 byte seed. + * Generate a Ed25519 keypair from a 32 byte seed. * * @param seed seed byte array */ @@ -79,14 +87,14 @@ export class Ed25519Keypair implements Keypair { } /** - * The public key for this keypair + * The public key for this Ed25519 keypair */ - getPublicKey(): PublicKey { - return new PublicKey(this.keypair.publicKey); + getPublicKey(): Ed25519PublicKey { + return new Ed25519PublicKey(this.keypair.publicKey); } /** - * Return the signature for the provided data. + * Return the signature for the provided data using Ed25519. */ signData(data: Base64DataBuffer): Base64DataBuffer { return new Base64DataBuffer( diff --git a/sdk/typescript/src/cryptography/ed25519-publickey.ts b/sdk/typescript/src/cryptography/ed25519-publickey.ts new file mode 100644 index 0000000000000..78318c72d0de5 --- /dev/null +++ b/sdk/typescript/src/cryptography/ed25519-publickey.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; +import { Buffer } from 'buffer'; +import { sha3_256 } from 'js-sha3'; +import { checkPublicKeyData, PublicKeyInitData, SIGNATURE_SCHEME_TO_FLAG } from './publickey'; + +const PUBLIC_KEY_SIZE = 32; + +/** + * An Ed25519 public key + */ +export class Ed25519PublicKey { + /** @internal */ + _bn: BN; + + /** + * Create a new Ed25519PublicKey object + * @param value ed25519 public key as buffer or base-64 encoded string + */ + constructor(value: PublicKeyInitData) { + if (checkPublicKeyData(value)) { + this._bn = value._bn; + } else { + if (typeof value === 'string') { + const buffer = Buffer.from(value, 'base64'); + if (buffer.length !== PUBLIC_KEY_SIZE) { + throw new Error( + `Invalid public key input. Expected ${PUBLIC_KEY_SIZE} bytes, got ${buffer.length}` + ); + } + this._bn = new BN(buffer); + } else { + this._bn = new BN(value); + } + let length = this._bn.byteLength(); + if (length != PUBLIC_KEY_SIZE) { + throw new Error( + `Invalid public key input. Expected ${PUBLIC_KEY_SIZE} bytes, got ${length}` + ); + } + } + } + + /** + * Checks if two Ed25519 public keys are equal + */ + equals(publicKey: Ed25519PublicKey): boolean { + return this._bn.eq(publicKey._bn); + } + + /** + * Return the base-64 representation of the Ed25519 public key + */ + toBase64(): string { + return this.toBuffer().toString('base64'); + } + + /** + * Return the byte array representation of the Ed25519 public key + */ + toBytes(): Uint8Array { + return this.toBuffer(); + } + + /** + * Return the Buffer representation of the Ed25519 public key + */ + toBuffer(): Buffer { + const b = this._bn.toArrayLike(Buffer); + if (b.length === PUBLIC_KEY_SIZE) { + return b; + } + + const zeroPad = Buffer.alloc(PUBLIC_KEY_SIZE); + b.copy(zeroPad, PUBLIC_KEY_SIZE - b.length); + return zeroPad; + } + + /** + * Return the base-64 representation of the Ed25519 public key + */ + toString(): string { + return this.toBase64(); + } + + /** + * Return the Sui address associated with this Ed25519 public key + */ + toSuiAddress(): string { + let tmp = new Uint8Array(PUBLIC_KEY_SIZE + 1); + tmp.set([SIGNATURE_SCHEME_TO_FLAG['ED25519']]); + tmp.set(this.toBytes(), 1); + return sha3_256(tmp).slice(0, 40); + } +} diff --git a/sdk/typescript/src/cryptography/keypair.ts b/sdk/typescript/src/cryptography/keypair.ts index 1167214a5a8cf..0bc133bde067d 100644 --- a/sdk/typescript/src/cryptography/keypair.ts +++ b/sdk/typescript/src/cryptography/keypair.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Base64DataBuffer } from '../serialization/base64'; -import { PublicKey } from './publickey'; +import { PublicKey, SignatureScheme } from './publickey'; /** * A keypair used for signing transactions. @@ -17,4 +17,9 @@ export interface Keypair { * Return the signature for the data */ signData(data: Base64DataBuffer): Base64DataBuffer; + + /** + * Get the key scheme of the keypair: Secp256k1 or ED25519 + */ + getKeyScheme(): SignatureScheme; } diff --git a/sdk/typescript/src/cryptography/publickey.ts b/sdk/typescript/src/cryptography/publickey.ts index 9f772a892144d..0b1dbefe11df1 100644 --- a/sdk/typescript/src/cryptography/publickey.ts +++ b/sdk/typescript/src/cryptography/publickey.ts @@ -1,136 +1,71 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 - import BN from 'bn.js'; -import { Buffer } from 'buffer'; -import { sha3_256 } from 'js-sha3'; /** - * Value to be converted into public key + * Value to be converted into public key. */ -export type PublicKeyInitData = - | number - | string - | Buffer - | Uint8Array - | Array - | PublicKeyData; + export type PublicKeyInitData = + | number + | string + | Buffer + | Uint8Array + | Array + | PublicKeyData; /** - * JSON object representation of PublicKey class - */ +* JSON object representation of PublicKey class. +*/ export type PublicKeyData = { - /** @internal */ - _bn: BN; + /** @internal */ + _bn: BN; }; -export const PUBLIC_KEY_SIZE = 32; -export const TYPE_BYTE = 0x00; - +/** + * A keypair used for signing transactions. + */ export type SignatureScheme = 'ED25519' | 'Secp256k1'; - -const SIGNATURE_SCHEME_TO_FLAG = { + +export const SIGNATURE_SCHEME_TO_FLAG = { ED25519: 0x00, Secp256k1: 0x01, }; -function isPublicKeyData(value: PublicKeyInitData): value is PublicKeyData { +export function checkPublicKeyData(value: PublicKeyInitData): value is PublicKeyData { return (value as PublicKeyData)._bn !== undefined; } /** * A public key */ -export class PublicKey { - /** @internal */ - _bn: BN; - + export interface PublicKey { /** - * Create a new PublicKey object - * @param value ed25519 public key as buffer or base-64 encoded string + * Checks if two public keys are equal */ - constructor(value: PublicKeyInitData) { - if (isPublicKeyData(value)) { - this._bn = value._bn; - } else { - if (typeof value === 'string') { - const buffer = Buffer.from(value, 'base64'); - if (buffer.length !== 32) { - throw new Error( - `Invalid public key input. Expected 32 bytes, got ${buffer.length}` - ); - } - this._bn = new BN(buffer); - } else { - this._bn = new BN(value); - } - if (this._bn.byteLength() > PUBLIC_KEY_SIZE) { - throw new Error(`Invalid public key input`); - } - } - } - - /** - * Checks if two publicKeys are equal - */ - equals(publicKey: PublicKey): boolean { - return this._bn.eq(publicKey._bn); - } + equals(publicKey: PublicKey): boolean; /** * Return the base-64 representation of the public key */ - toBase64(): string { - return this.toBuffer().toString('base64'); - } - + toBase64(): string; + /** * Return the byte array representation of the public key */ - toBytes(): Uint8Array { - return this.toBuffer(); - } + toBytes(): Uint8Array; /** * Return the Buffer representation of the public key */ - toBuffer(): Buffer { - const b = this._bn.toArrayLike(Buffer); - if (b.length === PUBLIC_KEY_SIZE) { - return b; - } - - const zeroPad = Buffer.alloc(PUBLIC_KEY_SIZE); - b.copy(zeroPad, PUBLIC_KEY_SIZE - b.length); - return zeroPad; - } - + toBuffer(): Buffer; + /** * Return the base-64 representation of the public key */ - toString(): string { - return this.toBase64(); - } + toString(): string; /** * Return the Sui address associated with this public key */ - toSuiAddress(scheme: SignatureScheme = 'ED25519'): string { - let tmp = new Uint8Array(PUBLIC_KEY_SIZE + 1); - tmp.set([SIGNATURE_SCHEME_TO_FLAG[scheme]]); - tmp.set(this.toBytes(), 1); - const hexHash = sha3_256(tmp); - const publicKeyBytes = new BN(hexHash, 16).toArray(undefined, 32); - // Only take the first 20 bytes - const addressBytes = publicKeyBytes.slice(0, 20); - return toHexString(addressBytes); - } -} - -// https://stackoverflow.com/questions/34309988/byte-array-to-hex-string-conversion-in-javascript -function toHexString(byteArray: number[]) { - return byteArray.reduce( - (output, elem) => output + ('0' + elem.toString(16)).slice(-2), - '' - ); + toSuiAddress(): string } diff --git a/sdk/typescript/src/cryptography/secp256k1-keypair.ts b/sdk/typescript/src/cryptography/secp256k1-keypair.ts new file mode 100644 index 0000000000000..91cb44d7aa609 --- /dev/null +++ b/sdk/typescript/src/cryptography/secp256k1-keypair.ts @@ -0,0 +1,123 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as secp from "@noble/secp256k1"; +import { Base64DataBuffer } from '../serialization/base64'; +import { Keypair } from './keypair'; +import { PublicKey, SignatureScheme } from './publickey'; +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from "@noble/hashes/sha256"; +import { Secp256k1PublicKey } from "./secp256k1-publickey"; + +secp.utils.hmacSha256Sync = (key: Uint8Array, ...msgs: Uint8Array[]) => { + const h = hmac.create(sha256, key); + msgs.forEach(msg => h.update(msg)); + return h.digest(); +}; + +/** + * Secp256k1 Keypair data + */ +export interface Secp256k1KeypairData { + publicKey: Uint8Array; + secretKey: Uint8Array; +} + +/** + * An Secp256k1 Keypair used for signing transactions. + */ +export class Secp256k1Keypair implements Keypair { + private keypair: Secp256k1KeypairData; + + /** + * Create a new keypair instance. + * Generate random keypair if no {@link Secp256k1Keypair} is provided. + * + * @param keypair secp256k1 keypair + */ + constructor(keypair?: Secp256k1KeypairData) { + if (keypair) { + this.keypair = keypair; + } else { + const secretKey: Uint8Array = secp.utils.randomPrivateKey(); + const publicKey: Uint8Array = secp.getPublicKey(secretKey, true); + + this.keypair = {publicKey, secretKey}; + } + } + + /** + * Get the key scheme of the keypair Secp256k1 + */ + getKeyScheme(): SignatureScheme { + return 'Secp256k1'; + } + + /** + * Generate a new random keypair + */ + static generate(): Secp256k1Keypair { + const secretKey = secp.utils.randomPrivateKey(); + const publicKey = secp.getPublicKey(secretKey, true); + + return new Secp256k1Keypair({publicKey, secretKey}); + } + + /** + * Create a keypair from a raw secret key byte array. + * + * This method should only be used to recreate a keypair from a previously + * generated secret key. Generating keypairs from a random seed should be done + * with the {@link Keypair.fromSeed} method. + * + * @throws error if the provided secret key is invalid and validation is not skipped. + * + * @param secretKey secret key byte array + * @param options: skip secret key validation + */ + + + static fromSecretKey( + secretKey: Uint8Array, + options?: { skipValidation?: boolean } + ): Secp256k1Keypair { + const publicKey: Uint8Array = secp.getPublicKey(secretKey, true); + if (!options || !options.skipValidation) { + const encoder = new TextEncoder(); + const signData = encoder.encode('sui validation'); + const msgHash = sha256(signData); + const signature = secp.signSync(msgHash, secretKey); + if (!secp.verify(signature, msgHash, publicKey)) { + throw new Error('Provided secretKey is invalid'); + } + } + return new Secp256k1Keypair({publicKey, secretKey}); + } + + /** + * Generate a keypair from a 32 byte seed. + * + * @param seed seed byte array + */ + static fromSeed(seed: Uint8Array): Secp256k1Keypair { + let publicKey = secp.getPublicKey(seed, true); + return new Secp256k1Keypair({publicKey, secretKey: seed}); + } + + /** + * The public key for this keypair + */ + getPublicKey(): PublicKey { + return new Secp256k1PublicKey(this.keypair.publicKey); + } + + /** + * Return the signature for the provided data. + */ + signData(data: Base64DataBuffer): Base64DataBuffer { + const msgHash = sha256(data.getData()); + return new Base64DataBuffer( + secp.signSync(msgHash, this.keypair.secretKey) + ); + } +} diff --git a/sdk/typescript/src/cryptography/secp256k1-publickey.ts b/sdk/typescript/src/cryptography/secp256k1-publickey.ts new file mode 100644 index 0000000000000..525a6a2c66fbf --- /dev/null +++ b/sdk/typescript/src/cryptography/secp256k1-publickey.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; +import { Buffer } from 'buffer'; +import { sha3_256 } from 'js-sha3'; +import { checkPublicKeyData, PublicKey, PublicKeyInitData, SIGNATURE_SCHEME_TO_FLAG } from './publickey'; + +const SECP256K1_PUBLIC_KEY_SIZE = 33; + +/** + * A Secp256k1 public key + */ +export class Secp256k1PublicKey implements PublicKey { + /** @internal */ + _bn: BN; + + /** + * Create a new Secp256k1PublicKey object + * @param value secp256k1 public key as buffer or base-64 encoded string + */ + constructor(value: PublicKeyInitData) { + if (checkPublicKeyData(value)) { + this._bn = value._bn; + } else { + if (typeof value === 'string') { + const buffer = Buffer.from(value, 'base64'); + if (buffer.length !== SECP256K1_PUBLIC_KEY_SIZE) { + throw new Error( + `Invalid public key input. Expected ${SECP256K1_PUBLIC_KEY_SIZE} bytes, got ${buffer.length}` + ); + } + this._bn = new BN(buffer); + } else { + this._bn = new BN(value); + } + let length = this._bn.byteLength(); + if (length != SECP256K1_PUBLIC_KEY_SIZE) { + throw new Error( + `Invalid public key input. Expected ${SECP256K1_PUBLIC_KEY_SIZE} bytes, got ${length}` + ); + } + } + } + + /** + * Checks if two Secp256k1 public keys are equal + */ + equals(publicKey: Secp256k1PublicKey): boolean { + return this._bn.eq(publicKey._bn); + } + + /** + * Return the base-64 representation of the Secp256k1 public key + */ + toBase64(): string { + return this.toBuffer().toString('base64'); + } + + /** + * Return the byte array representation of the Secp256k1 public key + */ + toBytes(): Uint8Array { + return this.toBuffer(); + } + + /** + * Return the Buffer representation of the Secp256k1 public key + */ + toBuffer(): Buffer { + const b = this._bn.toArrayLike(Buffer); + if (b.length === SECP256K1_PUBLIC_KEY_SIZE) { + return b; + } + + const zeroPad = Buffer.alloc(SECP256K1_PUBLIC_KEY_SIZE); + b.copy(zeroPad, SECP256K1_PUBLIC_KEY_SIZE - b.length); + return zeroPad; + } + + /** + * Return the base-64 representation of the Secp256k1 public key + */ + toString(): string { + return this.toBase64(); + } + + /** + * Return the Sui address associated with this Secp256k1 public key + */ + toSuiAddress(): string { + let tmp = new Uint8Array(SECP256K1_PUBLIC_KEY_SIZE + 1); + tmp.set([SIGNATURE_SCHEME_TO_FLAG['Secp256k1']]); + tmp.set(this.toBytes(), 1); + return sha3_256(tmp).slice(0, 40); + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 2d90e0788445c..ae180bf1a8c57 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 export * from './cryptography/ed25519-keypair'; +export * from './cryptography/secp256k1-keypair'; export * from './cryptography/keypair'; +export * from './cryptography/ed25519-publickey'; +export * from './cryptography/secp256k1-publickey'; export * from './cryptography/publickey'; export * from './providers/provider'; diff --git a/sdk/typescript/src/signers/raw-signer.ts b/sdk/typescript/src/signers/raw-signer.ts index 700a4a2e81f5c..3cd835c2682e2 100644 --- a/sdk/typescript/src/signers/raw-signer.ts +++ b/sdk/typescript/src/signers/raw-signer.ts @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { Ed25519Keypair } from '../cryptography/ed25519-keypair'; +import { Keypair } from '../cryptography/keypair'; import { Provider } from '../providers/provider'; import { Base64DataBuffer } from '../serialization/base64'; import { SuiAddress } from '../types'; @@ -10,10 +10,10 @@ import { SignerWithProvider } from './signer-with-provider'; import { TxnDataSerializer } from './txn-data-serializers/txn-data-serializer'; export class RawSigner extends SignerWithProvider { - private readonly keypair: Ed25519Keypair; + private readonly keypair: Keypair; constructor( - keypair: Ed25519Keypair, + keypair: Keypair, provider?: Provider, serializer?: TxnDataSerializer ) { @@ -27,7 +27,7 @@ export class RawSigner extends SignerWithProvider { async signData(data: Base64DataBuffer): Promise { return { - signatureScheme: 'ED25519', + signatureScheme: this.keypair.getKeyScheme(), signature: this.keypair.signData(data), pubKey: this.keypair.getPublicKey(), }; diff --git a/sdk/typescript/test/cryptography/publickey.test.ts b/sdk/typescript/test/cryptography/ed25519-publickey.test.ts similarity index 53% rename from sdk/typescript/test/cryptography/publickey.test.ts rename to sdk/typescript/test/cryptography/ed25519-publickey.test.ts index ee151f1dfc0ff..6043310eb376c 100644 --- a/sdk/typescript/test/cryptography/publickey.test.ts +++ b/sdk/typescript/test/cryptography/ed25519-publickey.test.ts @@ -2,7 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, it, expect } from 'vitest'; -import { PublicKey } from '../../src'; +import { Ed25519PublicKey } from '../../src'; + +// Test case generated against CLI: +// cargo build --bin sui +// ../sui/target/debug/sui client new-address ed25519 +// ../sui/target/debug/sui keytool list +let TEST_CASES = new Map([ + ["UdGRWooy48vGTs0HBokIis5NK+DUjiWc9ENUlcfCCBE=", "3415400a4bfdf924aefa55446e5f4cd6e9a9399f"], + ["0PTAfQmNiabgbak9U/stWZzKc5nsRqokda2qnV2DTfg=", "2e6dad710b343b8655825bc420783aaa5ade08c2"], + ["6L/l0uhGt//9cf6nLQ0+24Uv2qanX/R6tn7lWUJX1Xk=", "607a2403069d547c3fbba4b9e22793c7d78abb1f"], + ["6qZ88i8NJjaD+qZety3qXi4pLptGKS3wwO8bfDmUD+Y=", "7a4b0fd76cce17ef014d64ec5e073117bfc0b4de"], + ["RgdFhZXGe21x48rhe9X+Kh/WyFCo9ft6e9nQKZYHpi0=", "ecd7ef15f92a26bc8f22a88a7786fe1aae1051c6"], +]); const VALID_KEY_BASE64 = 'Uz39UFseB/B38iBwjesIU1JZxY6y+TRL9P84JFw41W4='; @@ -41,10 +53,11 @@ const BASE64_KEY_BYTES = [ 236, ]; -describe('PublicKey', () => { +describe('Ed25519PublicKey', () => { it('invalid', () => { + // public key length 33 is invalid for Ed25519 expect(() => { - new PublicKey([ + new Ed25519PublicKey([ 3, 0, 0, @@ -82,44 +95,51 @@ describe('PublicKey', () => { }).toThrow(); expect(() => { - new PublicKey( + new Ed25519PublicKey( '0x300000000000000000000000000000000000000000000000000000000000000000000' ); }).toThrow(); expect(() => { - new PublicKey( + new Ed25519PublicKey( '0x300000000000000000000000000000000000000000000000000000000000000' ); }).toThrow(); expect(() => { - new PublicKey( + new Ed25519PublicKey( '135693854574979916511997248057056142015550763280047535983739356259273198796800000' ); }).toThrow(); expect(() => { - new PublicKey('12345'); + new Ed25519PublicKey('12345'); }).toThrow(); }); it('toBase64', () => { - const key = new PublicKey(VALID_KEY_BASE64); + const key = new Ed25519PublicKey(VALID_KEY_BASE64); expect(key.toBase64()).toEqual(VALID_KEY_BASE64); expect(key.toString()).toEqual(VALID_KEY_BASE64); }); it('toBuffer', () => { - const key = new PublicKey(VALID_KEY_BASE64); + const key = new Ed25519PublicKey(VALID_KEY_BASE64); expect(key.toBuffer().length).toBe(32); - expect(new PublicKey(key.toBuffer()).equals(key)).toBe(true); + expect(new Ed25519PublicKey(key.toBuffer()).equals(key)).toBe(true); }); it('toSuiAddress', () => { - const key = new PublicKey(new Uint8Array(BASE64_KEY_BYTES)); + const key = new Ed25519PublicKey(new Uint8Array(BASE64_KEY_BYTES)); expect(key.toSuiAddress()).toEqual( '98fc1c8179b95274327069cf3b0ed051fb14e0bc' ); }); -}); + + TEST_CASES.forEach((address, base64) => { + it(`toSuiAddress from base64 public key ${address}`, () => { + const key = new Ed25519PublicKey(base64); + expect(key.toSuiAddress()).toEqual(address); + }); + }); +}); \ No newline at end of file diff --git a/sdk/typescript/test/cryptography/secp256k1-keypair.test.ts b/sdk/typescript/test/cryptography/secp256k1-keypair.test.ts new file mode 100644 index 0000000000000..761743f52ef41 --- /dev/null +++ b/sdk/typescript/test/cryptography/secp256k1-keypair.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Base64DataBuffer, Secp256k1Keypair } from '../../src'; +import { describe, it, expect } from 'vitest'; +import * as secp from "@noble/secp256k1"; + +// Test case from https://github.com/rust-bitcoin/rust-secp256k1/blob/master/examples/sign_verify.rs#L26 +const VALID_SECP256K1_SECRET_KEY = [ + 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, + 94, 203, 174, 253, 102, 39, 170, 146, 46, 252, 4, 143, 236, 12, 136, 28, +]; + +// Corresponding to the secret key above. +export const VALID_SECP256K1_PUBLIC_KEY = [ + 2, 29, 21, 35, 7, 198, 183, 43, 14, 208, 65, 139, 14, 112, 205, 128, 231, + 245, 41, 91, 141, 134, 245, 114, 45, 63, 82, 19, 251, 210, 57, 79, 54, +]; + +// Invalid private key with incorrect length +export const INVALID_SECP256K1_SECRET_KEY = Uint8Array.from(Array(31).fill(1)); + +// Invalid public key with incorrect length +export const INVALID_SECP256K1_PUBLIC_KEY = Uint8Array.from(Array(32).fill(1)); + +describe('secp256k1-keypair', () => { + it('new keypair', () => { + const keypair = new Secp256k1Keypair(); + expect(keypair.getPublicKey().toBytes().length).toBe(33); + expect(2).toEqual(2); + }); + + it('create keypair from secret key', () => { + const secret_key = Buffer.from(VALID_SECP256K1_SECRET_KEY); + const pub_key = Buffer.from(VALID_SECP256K1_PUBLIC_KEY); + let pub_key_base64 = pub_key.toString('base64'); + const keypair = Secp256k1Keypair.fromSecretKey(secret_key); + expect(keypair.getPublicKey().toBytes()).toEqual(Buffer.from(pub_key)); + expect(keypair.getPublicKey().toBase64()).toEqual(pub_key_base64); + }); + + it('creating keypair from invalid secret key throws error', () => { + const secret_key = Buffer.from(INVALID_SECP256K1_SECRET_KEY); + let secret_key_base64 = secret_key.toString('base64'); + const secretKey = Buffer.from(secret_key_base64, 'base64'); + expect(() => { + Secp256k1Keypair.fromSecretKey(secretKey); + }).toThrow('Expected 32 bytes of private key'); + }); + + it('generate keypair from random seed', () => { + const keypair = Secp256k1Keypair.fromSeed(Uint8Array.from(Array(32).fill(8))); + expect(keypair.getPublicKey().toBase64()).toEqual( + 'A/mR+UTR4ZVKf8i5v2Lg148BX0wHdi1QXiDmxFJgo2Yb' + ); + }); + + it('signature of data is valid', async () => { + const keypair = new Secp256k1Keypair(); + const signData = new Base64DataBuffer( + new TextEncoder().encode('hello world') + ); + + const msgHash = await secp.utils.sha256(signData.getData()); + const signature = keypair.signData(signData); + const isValid = secp.verify( + signature.getData(), + msgHash, + keypair.getPublicKey().toBytes() + ); + expect(isValid).toBeTruthy(); + }); +}); diff --git a/sdk/typescript/test/cryptography/secp256k1-publickey.test.ts b/sdk/typescript/test/cryptography/secp256k1-publickey.test.ts new file mode 100644 index 0000000000000..38aed6542f801 --- /dev/null +++ b/sdk/typescript/test/cryptography/secp256k1-publickey.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect } from 'vitest'; +import { Secp256k1PublicKey } from '../../src/cryptography/secp256k1-publickey'; +import { INVALID_SECP256K1_PUBLIC_KEY, VALID_SECP256K1_PUBLIC_KEY } from './secp256k1-keypair.test'; + +// Test case generated against CLI: +// cargo build --bin sui +// ../sui/target/debug/sui client new-address secp256k1 +// ../sui/target/debug/sui keytool list +let SECP_TEST_CASES = new Map([ + ["AwTC3jVFRxXc3RJIFgoQcv486QdqwYa8vBp4bgSq0gsI", "35057079b5dfc60d650768e2f4f92318f4ea5a77"], + ["A1F2CtldIGolO92Pm9yuxWXs5E07aX+6ZEHAnSuKOhii", "0187cf4234ff80862d5a1665d840df400fef29a0"], + ["Ak5rsa5Od4T6YFN/V3VIhZ/azMMYPkUilKQwc+RiaId+", "70eaff6b7973c57842c2272f00aa19af9f20dc1b"], + ["A4XbJ3fLvV/8ONsnLHAW1nORKsoCYsHaXv9FK1beMtvY", "deb28f733d9f59910cb210d56a46614f9dd28360"], +]); + describe('Secp256k1PublicKey', () => { + it('invalid', () => { + expect(() => { + new Secp256k1PublicKey(INVALID_SECP256K1_PUBLIC_KEY); + }).toThrow(); + + expect(() => { + const invalid_pubkey_buffer = Buffer.from(INVALID_SECP256K1_PUBLIC_KEY); + let invalid_pubkey_base64 = invalid_pubkey_buffer.toString('base64'); + new Secp256k1PublicKey(invalid_pubkey_base64); + }).toThrow(); + + expect(() => { + const pubkey_buffer = Buffer.from(VALID_SECP256K1_PUBLIC_KEY); + let wrong_encode = pubkey_buffer.toString('hex'); + new Secp256k1PublicKey(wrong_encode); + }).toThrow(); + + expect(() => { + new Secp256k1PublicKey('12345'); + }).toThrow(); + }); + + it('toBase64', () => { + const pub_key = Buffer.from(VALID_SECP256K1_PUBLIC_KEY); + let pub_key_base64 = pub_key.toString('base64'); + const key = new Secp256k1PublicKey(pub_key_base64); + expect(key.toBase64()).toEqual(pub_key_base64); + expect(key.toString()).toEqual(pub_key_base64); + }); + + it('toBuffer', () => { + const pub_key = Buffer.from(VALID_SECP256K1_PUBLIC_KEY); + let pub_key_base64 = pub_key.toString('base64'); + const key = new Secp256k1PublicKey(pub_key_base64); + expect(key.toBuffer().length).toBe(33); + expect(new Secp256k1PublicKey(key.toBuffer()).equals(key)).toBe(true); + }); + + SECP_TEST_CASES.forEach((address, base64) => { + it(`toSuiAddress from base64 public key ${address}`, () => { + const key = new Secp256k1PublicKey(base64); + expect(key.toSuiAddress()).toEqual(address); + }); + }); +}); diff --git a/sdk/typescript/test/signers/raw-signer.test.ts b/sdk/typescript/test/signers/raw-signer.test.ts index 7b39fda03736b..837003dfbf33d 100644 --- a/sdk/typescript/test/signers/raw-signer.test.ts +++ b/sdk/typescript/test/signers/raw-signer.test.ts @@ -3,10 +3,11 @@ import nacl from 'tweetnacl'; import { describe, it, expect } from 'vitest'; -import { Base64DataBuffer, Ed25519Keypair, RawSigner } from '../../src'; +import { Base64DataBuffer, Ed25519Keypair, RawSigner, Secp256k1Keypair } from '../../src'; +import * as secp from "@noble/secp256k1"; describe('RawSigner', () => { - it('signData', async () => { + it('Ed25519 keypair signData', async () => { const keypair = new Ed25519Keypair(); const signData = new Base64DataBuffer( new TextEncoder().encode('hello world') @@ -20,4 +21,20 @@ describe('RawSigner', () => { ); expect(isValid).toBeTruthy(); }); + + it('Secp256k1 keypair signData', async () => { + const keypair = new Secp256k1Keypair(); + const signData = new Base64DataBuffer( + new TextEncoder().encode('hello world') + ); + const msgHash = await secp.utils.sha256(signData.getData()); + const signer = new RawSigner(keypair); + const { signature, pubKey } = await signer.signData(signData); + const isSecpValid = secp.verify( + signature.getData(), + msgHash, + pubKey.toBytes() + ); + expect(isSecpValid).toBeTruthy(); + }); });