From 512a9eee513243ebb2aac2fab898abcbb3144fe7 Mon Sep 17 00:00:00 2001 From: Warren He Date: Mon, 14 Oct 2024 16:21:38 -0700 Subject: [PATCH 01/20] ts-web: add changelogs for prior changes --- client-sdk/ts-web/core/docs/changelog.md | 16 ++++++++++++++++ client-sdk/ts-web/rt/docs/changelog.md | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/client-sdk/ts-web/core/docs/changelog.md b/client-sdk/ts-web/core/docs/changelog.md index 5caff8786a..6f7624318a 100644 --- a/client-sdk/ts-web/core/docs/changelog.md +++ b/client-sdk/ts-web/core/docs/changelog.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased changes + +New features: + +- Hashing and many related functions that internally need to compute a hash, + such as getting the address of a public key, are now declared as + synchronous. + We had implementations that used synchronous hashing libraries all along, + but this is us giving up on eventually using the Web Crypto API for + SHA-512/256. + +Little things: + +- We're switching lots of cryptography dependencies to noble cryptography + libraries. + ## v1.1.0 Spotlight change: diff --git a/client-sdk/ts-web/rt/docs/changelog.md b/client-sdk/ts-web/rt/docs/changelog.md index e8c80af10e..671026de0f 100644 --- a/client-sdk/ts-web/rt/docs/changelog.md +++ b/client-sdk/ts-web/rt/docs/changelog.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased changes + +New features: + +- Functions that internally need to compute a hash, such as + `address.fromSigspec`, are declared as synchronous now. +- secp256k1 verification is declared as synchronous now. + +Little things: + +- We're switching lots of cryptography dependencies to noble cryptography + libraries. + ## v1.1.0 Spotlight change: From 47f8096265f62fa10d681e98ce5dab7698eaae7f Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 15 Oct 2024 17:19:48 -0700 Subject: [PATCH 02/20] ts-web: remove await from calls that are now sync --- .../ts-web/core/playground/src/startPlayground.mjs | 14 +++++++------- .../ts-web/ext-utils/sample-page/src/index.js | 6 +++--- client-sdk/ts-web/rt/playground/src/index.js | 14 +++++++------- client-sdk/ts-web/rt/test/address.test.ts | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/client-sdk/ts-web/core/playground/src/startPlayground.mjs b/client-sdk/ts-web/core/playground/src/startPlayground.mjs index 3402b995a4..818e0d95c8 100644 --- a/client-sdk/ts-web/core/playground/src/startPlayground.mjs +++ b/client-sdk/ts-web/core/playground/src/startPlayground.mjs @@ -43,19 +43,19 @@ export async function startPlayground() { console.log('chain context', chainContext); const genesis = await nic.consensusGetGenesisDocument(); - const ourChainContext = await oasis.genesis.chainContext(genesis); + const ourChainContext = oasis.genesis.chainContext(genesis); console.log('computed from genesis', ourChainContext); if (ourChainContext !== chainContext) throw new Error('computed chain context mismatch'); const nonce = await nic.consensusGetSignerNonce({ - account_address: await oasis.staking.addressFromPublicKey(src.public()), + account_address: oasis.staking.addressFromPublicKey(src.public()), height: oasis.consensus.HEIGHT_LATEST, }); console.log('nonce', nonce); const account = await nic.stakingAccount({ height: oasis.consensus.HEIGHT_LATEST, - owner: await oasis.staking.addressFromPublicKey(src.public()), + owner: oasis.staking.addressFromPublicKey(src.public()), }); console.log('account', account); if ((account.general?.nonce ?? 0) !== nonce) throw new Error('nonce mismatch'); @@ -64,7 +64,7 @@ export async function startPlayground() { tw.setNonce(account.general?.nonce ?? 0); tw.setFeeAmount(oasis.quantity.fromBigInt(0n)); tw.setBody({ - to: await oasis.staking.addressFromPublicKey(dst.public()), + to: oasis.staking.addressFromPublicKey(dst.public()), amount: oasis.quantity.fromBigInt(0n), }); @@ -75,7 +75,7 @@ export async function startPlayground() { await tw.sign(new oasis.signature.BlindContextSigner(src), chainContext); console.log('singed transaction', tw.signedTransaction); - console.log('hash', await tw.hash()); + console.log('hash', tw.hash()); await tw.submit(nic); console.log('sent'); @@ -100,9 +100,9 @@ export async function startPlayground() { signedTransaction, ); console.log({ - hash: await oasis.consensus.hashSignedTransaction(signedTransaction), + hash: oasis.consensus.hashSignedTransaction(signedTransaction), from: oasis.staking.addressToBech32( - await oasis.staking.addressFromPublicKey( + oasis.staking.addressFromPublicKey( signedTransaction.signature.public_key, ), ), diff --git a/client-sdk/ts-web/ext-utils/sample-page/src/index.js b/client-sdk/ts-web/ext-utils/sample-page/src/index.js index 0d9d0d52c1..eb1b10b5fa 100644 --- a/client-sdk/ts-web/ext-utils/sample-page/src/index.js +++ b/client-sdk/ts-web/ext-utils/sample-page/src/index.js @@ -44,7 +44,7 @@ export const playground = (async function () { console.log('public key base64', oasis.misc.toBase64(publicKey)); console.log( 'address bech32', - oasis.staking.addressToBech32(await oasis.staking.addressFromPublicKey(publicKey)), + oasis.staking.addressToBech32(oasis.staking.addressFromPublicKey(publicKey)), ); const dst = oasis.signature.NaclSigner.fromRandom('this key is not important'); @@ -54,7 +54,7 @@ export const playground = (async function () { .setFeeAmount(oasis.quantity.fromBigInt(102n)) .setFeeGas(103n) .setBody({ - to: await oasis.staking.addressFromPublicKey(dst.public()), + to: oasis.staking.addressFromPublicKey(dst.public()), amount: oasis.quantity.fromBigInt(104n), }); console.log('requesting signature'); @@ -65,7 +65,7 @@ export const playground = (async function () { const rtw = new oasisRT.accounts.Wrapper(oasis.misc.fromString('fake-runtime-id-for-testing')) .callTransfer() .setBody({ - to: await oasis.staking.addressFromPublicKey(dst.public()), + to: oasis.staking.addressFromPublicKey(dst.public()), amount: [oasis.quantity.fromBigInt(105n), oasis.misc.fromString('TEST')], }) .setSignerInfo([ diff --git a/client-sdk/ts-web/rt/playground/src/index.js b/client-sdk/ts-web/rt/playground/src/index.js index 86ace4c082..96be6baeed 100644 --- a/client-sdk/ts-web/rt/playground/src/index.js +++ b/client-sdk/ts-web/rt/playground/src/index.js @@ -111,7 +111,7 @@ export const playground = (async function () { console.log('signature', signature); console.log( 'valid', - await oasisRT.signatureSecp256k1.verify('test context', message, signature, publicKey), + oasisRT.signatureSecp256k1.verify('test context', message, signature, publicKey), ); } @@ -120,7 +120,7 @@ export const playground = (async function () { const runtimeID = oasis.misc.fromHex( '8000000000000000000000000000000000000000000000000000000000000000', ); - const chainContext = await oasisRT.transaction.deriveChainContext( + const chainContext = oasisRT.transaction.deriveChainContext( runtimeID, '643fb06848be7e970af3b5b2d772eb8cfb30499c8162bc18ac03df2f5e22520e', ); @@ -146,12 +146,12 @@ export const playground = (async function () { } const alice = oasis.signature.NaclSigner.fromSeed( - await oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: alice')), + oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: alice')), 'this key is not important', ); const csAlice = new oasis.signature.BlindContextSigner(alice); const bob = oasis.signature.NaclSigner.fromSeed( - await oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: bob')), + oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: bob')), 'this key is not important', ); const csBob = new oasis.signature.BlindContextSigner(bob); @@ -203,7 +203,7 @@ export const playground = (async function () { const nonce1 = await accountsWrapper .queryNonce() .setArgs({ - address: await oasis.staking.addressFromPublicKey(alice.public()), + address: oasis.staking.addressFromPublicKey(alice.public()), }) .query(nic); const siAlice1 = /** @type {oasisRT.types.SignerInfo} */ ({ @@ -253,7 +253,7 @@ export const playground = (async function () { const nonce2 = await accountsWrapper .queryNonce() .setArgs({ - address: await oasis.staking.addressFromPublicKey(alice.public()), + address: oasis.staking.addressFromPublicKey(alice.public()), }) .query(nic); const siAlice2 = /** @type {oasisRT.types.SignerInfo} */ ({ @@ -347,7 +347,7 @@ export const playground = (async function () { ], threshold: 2, }); - const addr = await oasisRT.address.fromMultisigConfig(msConfig); + const addr = oasisRT.address.fromMultisigConfig(msConfig); const addrBech32 = oasis.staking.addressToBech32(addr); const refBech32 = 'oasis1qpcprk8jxpsjxw9fadxvzrv9ln7td69yus8rmtux'; console.log('address for sample config', addrBech32, 'reference', refBech32); diff --git a/client-sdk/ts-web/rt/test/address.test.ts b/client-sdk/ts-web/rt/test/address.test.ts index 0361945223..a28bb124de 100644 --- a/client-sdk/ts-web/rt/test/address.test.ts +++ b/client-sdk/ts-web/rt/test/address.test.ts @@ -4,7 +4,7 @@ describe('address', () => { describe('ed25519', () => { it('Should derive the address correctly', async () => { const pk = Buffer.from('utrdHlX///////////////////////////////////8=', 'base64'); - const address = await oasisRT.address.fromSigspec({ed25519: new Uint8Array(pk)}); + const address = oasisRT.address.fromSigspec({ed25519: new Uint8Array(pk)}); expect(oasisRT.address.toBech32(address)).toEqual( 'oasis1qryqqccycvckcxp453tflalujvlf78xymcdqw4vz', ); @@ -14,7 +14,7 @@ describe('address', () => { describe('secp256k1eth', () => { it('Should derive the address correctly', async () => { const pk = Buffer.from('Arra3R5V////////////////////////////////////', 'base64'); - const address = await oasisRT.address.fromSigspec({secp256k1eth: new Uint8Array(pk)}); + const address = oasisRT.address.fromSigspec({secp256k1eth: new Uint8Array(pk)}); expect(oasisRT.address.toBech32(address)).toEqual( 'oasis1qzd7akz24n6fxfhdhtk977s5857h3c6gf5583mcg', ); From 2d448611af697860781876eaed5b8e8500577408 Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 13:44:33 -0700 Subject: [PATCH 03/20] ts-web/core: restore async verify --- client-sdk/ts-web/core/src/common.ts | 11 +++++++---- client-sdk/ts-web/core/src/consensus.ts | 4 ++-- client-sdk/ts-web/core/src/signature.ts | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client-sdk/ts-web/core/src/common.ts b/client-sdk/ts-web/core/src/common.ts index ee817e48ca..cd65025b2b 100644 --- a/client-sdk/ts-web/core/src/common.ts +++ b/client-sdk/ts-web/core/src/common.ts @@ -72,8 +72,8 @@ export const IDENTITY_MODULE_NAME = 'identity'; */ export const IDENTITY_ERR_CERTIFICATE_ROTATION_FORBIDDEN_CODE = 1; -export function openSignedEntity(context: string, signed: types.SignatureSigned) { - return misc.fromCBOR(signature.openSigned(context, signed)) as types.Entity; +export async function openSignedEntity(context: string, signed: types.SignatureSigned) { + return misc.fromCBOR(await signature.openSigned(context, signed)) as types.Entity; } export async function signSignedEntity( @@ -84,8 +84,11 @@ export async function signSignedEntity( return await signature.signSigned(signer, context, misc.toCBOR(entity)); } -export function openMultiSignedNode(context: string, multiSigned: types.SignatureMultiSigned) { - return misc.fromCBOR(signature.openMultiSigned(context, multiSigned)) as types.Node; +export async function openMultiSignedNode( + context: string, + multiSigned: types.SignatureMultiSigned, +) { + return misc.fromCBOR(await signature.openMultiSigned(context, multiSigned)) as types.Node; } export async function signMultiSignedNode( diff --git a/client-sdk/ts-web/core/src/consensus.ts b/client-sdk/ts-web/core/src/consensus.ts index da3861e568..f014216aba 100644 --- a/client-sdk/ts-web/core/src/consensus.ts +++ b/client-sdk/ts-web/core/src/consensus.ts @@ -91,9 +91,9 @@ export const TRANSACTION_ERR_GAS_PRICE_TOO_LOW_CODE = 3; */ export const TRANSACTION_ERR_UPGRADE_PENDING = 4; -export function openSignedTransaction(chainContext: string, signed: types.SignatureSigned) { +export async function openSignedTransaction(chainContext: string, signed: types.SignatureSigned) { const context = signature.combineChainContext(TRANSACTION_SIGNATURE_CONTEXT, chainContext); - return misc.fromCBOR(signature.openSigned(context, signed)) as types.ConsensusTransaction; + return misc.fromCBOR(await signature.openSigned(context, signed)) as types.ConsensusTransaction; } export async function signSignedTransaction( diff --git a/client-sdk/ts-web/core/src/signature.ts b/client-sdk/ts-web/core/src/signature.ts index c05a89b032..13587a4a0c 100644 --- a/client-sdk/ts-web/core/src/signature.ts +++ b/client-sdk/ts-web/core/src/signature.ts @@ -24,7 +24,7 @@ export interface ContextSigner { sign(context: string, message: Uint8Array): Promise; } -export function verify( +export async function verify( publicKey: Uint8Array, context: string, message: Uint8Array, @@ -36,8 +36,8 @@ export function verify( return sigOk; } -export function openSigned(context: string, signed: types.SignatureSigned) { - const sigOk = verify( +export async function openSigned(context: string, signed: types.SignatureSigned) { + const sigOk = await verify( signed.signature.public_key, context, signed.untrusted_raw_value, @@ -57,7 +57,7 @@ export async function signSigned(signer: ContextSigner, context: string, rawValu } as types.SignatureSigned; } -export function openMultiSigned(context: string, multiSigned: types.SignatureMultiSigned) { +export async function openMultiSigned(context: string, multiSigned: types.SignatureMultiSigned) { const signerMessage = prepareSignerMessage(context, multiSigned.untrusted_raw_value); for (const signature of multiSigned.signatures) { const sigOk = nacl.sign.detached.verify( From b5f19a46da6a5273d550f1e4fa70751d93b16f80 Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 13:55:43 -0700 Subject: [PATCH 04/20] ts-web/core: signature extract verifyPrepared --- client-sdk/ts-web/core/src/signature.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client-sdk/ts-web/core/src/signature.ts b/client-sdk/ts-web/core/src/signature.ts index 13587a4a0c..455f076c21 100644 --- a/client-sdk/ts-web/core/src/signature.ts +++ b/client-sdk/ts-web/core/src/signature.ts @@ -24,6 +24,14 @@ export interface ContextSigner { sign(context: string, message: Uint8Array): Promise; } +async function verifyPrepared( + publicKey: Uint8Array, + signerMessage: Uint8Array, + signature: Uint8Array, +) { + return nacl.sign.detached.verify(signerMessage, signature, publicKey); +} + export async function verify( publicKey: Uint8Array, context: string, @@ -31,7 +39,7 @@ export async function verify( signature: Uint8Array, ) { const signerMessage = prepareSignerMessage(context, message); - const sigOk = nacl.sign.detached.verify(signerMessage, signature, publicKey); + const sigOk = await verifyPrepared(publicKey, signerMessage, signature); return sigOk; } @@ -60,10 +68,10 @@ export async function signSigned(signer: ContextSigner, context: string, rawValu export async function openMultiSigned(context: string, multiSigned: types.SignatureMultiSigned) { const signerMessage = prepareSignerMessage(context, multiSigned.untrusted_raw_value); for (const signature of multiSigned.signatures) { - const sigOk = nacl.sign.detached.verify( + const sigOk = await verifyPrepared( + signature.public_key, signerMessage, signature.signature, - signature.public_key, ); if (!sigOk) throw new Error('signature verification failed'); } From 5653ee2553ea4f5c9724a56879adb03e9a61da26 Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 15:19:52 -0700 Subject: [PATCH 05/20] ts-web/core: verifyPrepared web crypto impl --- client-sdk/ts-web/core/src/signature.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client-sdk/ts-web/core/src/signature.ts b/client-sdk/ts-web/core/src/signature.ts index 455f076c21..04d22cd7e7 100644 --- a/client-sdk/ts-web/core/src/signature.ts +++ b/client-sdk/ts-web/core/src/signature.ts @@ -29,7 +29,8 @@ async function verifyPrepared( signerMessage: Uint8Array, signature: Uint8Array, ) { - return nacl.sign.detached.verify(signerMessage, signature, publicKey); + const publicCK = await crypto.subtle.importKey('raw', publicKey, {name: 'Ed25519'}, true, ['verify']); + return await crypto.subtle.verify({name: 'Ed25519'}, publicCK, signature, signerMessage); } export async function verify( From 7349f6f2e3c1170aa10b5f1b39588b3adae300ed Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 15:21:30 -0700 Subject: [PATCH 06/20] ts-web: move NaclSigner to signer-tweetnacl --- client-sdk/ts-web/core/src/signature.ts | 64 ------------------ client-sdk/ts-web/package-lock.json | 31 ++++++++- client-sdk/ts-web/package.json | 3 +- client-sdk/ts-web/signer-tweetnacl/.gitignore | 3 + .../ts-web/signer-tweetnacl/.prettierrc.js | 7 ++ .../ts-web/signer-tweetnacl/package.json | 29 +++++++++ .../ts-web/signer-tweetnacl/src/index.ts | 65 +++++++++++++++++++ .../ts-web/signer-tweetnacl/tsconfig.json | 20 ++++++ 8 files changed, 156 insertions(+), 66 deletions(-) create mode 100644 client-sdk/ts-web/signer-tweetnacl/.gitignore create mode 100644 client-sdk/ts-web/signer-tweetnacl/.prettierrc.js create mode 100644 client-sdk/ts-web/signer-tweetnacl/package.json create mode 100644 client-sdk/ts-web/signer-tweetnacl/src/index.ts create mode 100644 client-sdk/ts-web/signer-tweetnacl/tsconfig.json diff --git a/client-sdk/ts-web/core/src/signature.ts b/client-sdk/ts-web/core/src/signature.ts index 04d22cd7e7..e4a4bc85c7 100644 --- a/client-sdk/ts-web/core/src/signature.ts +++ b/client-sdk/ts-web/core/src/signature.ts @@ -1,5 +1,3 @@ -import * as nacl from 'tweetnacl'; - import * as hash from './hash'; import * as misc from './misc'; import * as types from './types'; @@ -114,68 +112,6 @@ export class BlindContextSigner implements ContextSigner { } } -/** - * An in-memory signer based on tweetnacl. We've included this for development. - */ -export class NaclSigner implements Signer { - key: nacl.SignKeyPair; - - constructor(key: nacl.SignKeyPair, note: string) { - if (note !== 'this key is not important') throw new Error('insecure signer implementation'); - this.key = key; - } - - /** - * Generate a keypair from a random seed - * @param note Set to 'this key is not important' to acknowledge the risks - * @returns Instance of NaclSigner - */ - static fromRandom(note: string) { - const secret = new Uint8Array(32); - crypto.getRandomValues(secret); - return NaclSigner.fromSeed(secret, note); - } - - /** - * Instanciate from a given secret - * @param secret 64 bytes ed25519 secret (h) that will be used to sign messages - * @param note Set to 'this key is not important' to acknowledge the risks - * @returns Instance of NaclSigner - */ - static fromSecret(secret: Uint8Array, note: string) { - const key = nacl.sign.keyPair.fromSecretKey(secret); - return new NaclSigner(key, note); - } - - /** - * Instanciate from a given seed - * @param seed 32 bytes ed25519 seed (k) that will deterministically generate a private key - * @param note Set to 'this key is not important' to acknowledge the risks - * @returns Instance of NaclSigner - */ - static fromSeed(seed: Uint8Array, note: string) { - const key = nacl.sign.keyPair.fromSeed(seed); - return new NaclSigner(key, note); - } - - /** - * Returns the 32 bytes public key of this key pair - * @returns Public key - */ - public(): Uint8Array { - return this.key.publicKey; - } - - /** - * Signs the given message - * @param message Bytes to sign - * @returns Signed message - */ - async sign(message: Uint8Array): Promise { - return nacl.sign.detached(message, this.key.secretKey); - } -} - export type MessageHandlerBare = (v: PARSED) => void; export type MessageHandlersBare = {[context: string]: MessageHandlerBare}; export type MessageHandlerWithChainContext = (chainContext: string, v: PARSED) => void; diff --git a/client-sdk/ts-web/package-lock.json b/client-sdk/ts-web/package-lock.json index 5f856631aa..90c8ccbcd8 100644 --- a/client-sdk/ts-web/package-lock.json +++ b/client-sdk/ts-web/package-lock.json @@ -8,7 +8,8 @@ "core", "ext-utils", "rt", - "signer-ledger" + "signer-ledger", + "signer-tweetnacl" ] }, "core": { @@ -1314,6 +1315,10 @@ "@ledgerhq/hw-transport": "^6.1.0" } }, + "node_modules/@oasisprotocol/signer-tweetnacl": { + "resolved": "signer-tweetnacl", + "link": true + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -9486,6 +9491,20 @@ "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.1.0" } + }, + "signer-tweetnacl": { + "name": "@oasisprotocol/signer-tweetnacl", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@oasisprotocol/client": "^1.1.0", + "tweetnacl": "^1.0.3" + }, + "devDependencies": { + "prettier": "^3.3.3", + "typedoc": "^0.26.7", + "typescript": "^5.6.2" + } } }, "dependencies": { @@ -10562,6 +10581,16 @@ "@ledgerhq/hw-transport": "^6.1.0" } }, + "@oasisprotocol/signer-tweetnacl": { + "version": "file:signer-tweetnacl", + "requires": { + "@oasisprotocol/client": "^1.1.0", + "prettier": "^3.3.3", + "tweetnacl": "^1.0.3", + "typedoc": "^0.26.7", + "typescript": "^5.6.2" + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", diff --git a/client-sdk/ts-web/package.json b/client-sdk/ts-web/package.json index cbc6046d22..fb9e15a1d3 100644 --- a/client-sdk/ts-web/package.json +++ b/client-sdk/ts-web/package.json @@ -3,6 +3,7 @@ "core", "ext-utils", "rt", - "signer-ledger" + "signer-ledger", + "signer-tweetnacl" ] } diff --git a/client-sdk/ts-web/signer-tweetnacl/.gitignore b/client-sdk/ts-web/signer-tweetnacl/.gitignore new file mode 100644 index 0000000000..9c00274ec2 --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +/playground/dist/main.js diff --git a/client-sdk/ts-web/signer-tweetnacl/.prettierrc.js b/client-sdk/ts-web/signer-tweetnacl/.prettierrc.js new file mode 100644 index 0000000000..7ff25e0fb3 --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + bracketSpacing: false, + printWidth: 100, + semi: true, + singleQuote: true, + tabWidth: 4, +}; diff --git a/client-sdk/ts-web/signer-tweetnacl/package.json b/client-sdk/ts-web/signer-tweetnacl/package.json new file mode 100644 index 0000000000..5df3c30ed5 --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/package.json @@ -0,0 +1,29 @@ +{ + "name": "@oasisprotocol/signer-tweetnacl", + "version": "1.0.0", + "license": "Apache-2.0", + "homepage": "https://github.com/oasisprotocol/oasis-sdk/tree/main/client-sdk/ts-web/signer-tweetnacl", + "repository": { + "type": "git", + "url": "https://github.com/oasisprotocol/oasis-sdk.git", + "directory": "client-sdk/ts-web/signer-tweetnacl" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "scripts": { + "prepare": "tsc", + "fmt": "prettier --write src", + "lint": "prettier --check src" + }, + "dependencies": { + "@oasisprotocol/client": "^1.1.0", + "tweetnacl": "^1.0.3" + }, + "devDependencies": { + "prettier": "^3.3.3", + "typedoc": "^0.26.7", + "typescript": "^5.6.2" + } +} diff --git a/client-sdk/ts-web/signer-tweetnacl/src/index.ts b/client-sdk/ts-web/signer-tweetnacl/src/index.ts new file mode 100644 index 0000000000..244d77c89c --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/src/index.ts @@ -0,0 +1,65 @@ +import * as nacl from 'tweetnacl'; + +import * as oasis from '@oasisprotocol/client'; + +/** + * An in-memory signer based on tweetnacl. We've included this for development. + */ +export class NaclSigner implements oasis.signature.Signer { + key: nacl.SignKeyPair; + + constructor(key: nacl.SignKeyPair, note: string) { + if (note !== 'this key is not important') throw new Error('insecure signer implementation'); + this.key = key; + } + + /** + * Generate a keypair from a random seed + * @param note Set to 'this key is not important' to acknowledge the risks + * @returns Instance of NaclSigner + */ + static fromRandom(note: string) { + const secret = new Uint8Array(32); + crypto.getRandomValues(secret); + return NaclSigner.fromSeed(secret, note); + } + + /** + * Instanciate from a given secret + * @param secret 64 bytes ed25519 secret (h) that will be used to sign messages + * @param note Set to 'this key is not important' to acknowledge the risks + * @returns Instance of NaclSigner + */ + static fromSecret(secret: Uint8Array, note: string) { + const key = nacl.sign.keyPair.fromSecretKey(secret); + return new NaclSigner(key, note); + } + + /** + * Instanciate from a given seed + * @param seed 32 bytes ed25519 seed (k) that will deterministically generate a private key + * @param note Set to 'this key is not important' to acknowledge the risks + * @returns Instance of NaclSigner + */ + static fromSeed(seed: Uint8Array, note: string) { + const key = nacl.sign.keyPair.fromSeed(seed); + return new NaclSigner(key, note); + } + + /** + * Returns the 32 bytes public key of this key pair + * @returns Public key + */ + public(): Uint8Array { + return this.key.publicKey; + } + + /** + * Signs the given message + * @param message Bytes to sign + * @returns Signed message + */ + async sign(message: Uint8Array): Promise { + return nacl.sign.detached(message, this.key.secretKey); + } +} diff --git a/client-sdk/ts-web/signer-tweetnacl/tsconfig.json b/client-sdk/ts-web/signer-tweetnacl/tsconfig.json new file mode 100644 index 0000000000..7ecd805219 --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "strict": true, + "module": "CommonJS", + "target": "ES2020", + "declaration": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*" + ], + "typedocOptions": { + "entryPoints": ["src/index.ts"], + "out": "docs/api", + "excludeInternal": true, + "excludePrivate": true + } +} From 8d4d2b86425259727e15d84133248154f18edeb2 Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 16:42:17 -0700 Subject: [PATCH 07/20] ts-web/core: add WebCryptoSigner --- client-sdk/ts-web/core/src/signature.ts | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/client-sdk/ts-web/core/src/signature.ts b/client-sdk/ts-web/core/src/signature.ts index e4a4bc85c7..215548f331 100644 --- a/client-sdk/ts-web/core/src/signature.ts +++ b/client-sdk/ts-web/core/src/signature.ts @@ -112,6 +112,86 @@ export class BlindContextSigner implements ContextSigner { } } +export class WebCryptoSigner implements Signer { + privateCK: CryptoKey; + publicKey: Uint8Array; + + constructor(privateCK: CryptoKey, publicKey: Uint8Array) { + this.privateCK = privateCK; + this.publicKey = publicKey; + } + + /** + * Create a CryptoKeyPair from a 32-byte private key. + */ + static async keyPairFromPrivateKey(privateKey: Uint8Array) { + const privateDER = misc.concat( + new Uint8Array([ + // PrivateKeyInfo + 0x30, 0x2e, + // version 0 + 0x02, 0x01, 0x00, + // privateKeyAlgorithm 1.3.101.112 + 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, + // privateKey + 0x04, 0x22, 0x04, 0x20, + ]), + privateKey, + ); + const privateCK = await crypto.subtle.importKey('pkcs8', privateDER, {name: 'Ed25519'}, true, ['sign']); + const privateJWK = await crypto.subtle.exportKey('jwk', privateCK); + const publicJWK = { + kty: privateJWK.kty, + crv: privateJWK.crv, + x: privateJWK.x, + } as JsonWebKey; + const publicCK = await crypto.subtle.importKey('jwk', publicJWK, {name: 'Ed25519'}, true, ['verify']); + return { + publicKey: publicCK, + privateKey: privateCK, + } as CryptoKeyPair; + } + + /** + * Get the public key from a CryptoKeyPair. + */ + static async publicKeyFromKeyPair(keyPair: CryptoKeyPair) { + return new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)); + } + + /** + * Create an instance with a newly generated key. + */ + static async generate(extractable: boolean) { + const keyPair = await crypto.subtle.generateKey({name: 'Ed25519'}, extractable, ['sign', 'verify']) as CryptoKeyPair; + return await WebCryptoSigner.fromKeyPair(keyPair); + } + + /** + * Create an instance from a CryptoKeyPair. + */ + static async fromKeyPair(keyPair: CryptoKeyPair) { + const publicKey = await WebCryptoSigner.publicKeyFromKeyPair(keyPair); + return new WebCryptoSigner(keyPair.privateKey, publicKey); + } + + /** + * Create an instance from a 32-byte private key. + */ + static async fromPrivateKey(privateKey: Uint8Array) { + const keyPair = await WebCryptoSigner.keyPairFromPrivateKey(privateKey); + return await WebCryptoSigner.fromKeyPair(keyPair); + } + + public(): Uint8Array { + return this.publicKey; + } + + async sign(message: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.sign({name: 'Ed25519'}, this.privateCK, message)); + } +} + export type MessageHandlerBare = (v: PARSED) => void; export type MessageHandlersBare = {[context: string]: MessageHandlerBare}; export type MessageHandlerWithChainContext = (chainContext: string, v: PARSED) => void; From 225887a19c6fe0f61d70c914e5db1c1dc4e39a83 Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 8 Oct 2024 16:43:01 -0700 Subject: [PATCH 08/20] ts-web: playground use WebCryptoSigner --- client-sdk/ts-web/core/playground/src/startPlayground.mjs | 4 ++-- client-sdk/ts-web/ext-utils/sample-page/src/index.js | 2 +- client-sdk/ts-web/rt/playground/src/consensus.js | 3 +-- client-sdk/ts-web/rt/playground/src/index.js | 6 ++---- client-sdk/ts-web/signer-ledger/playground/src/index.js | 2 +- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/client-sdk/ts-web/core/playground/src/startPlayground.mjs b/client-sdk/ts-web/core/playground/src/startPlayground.mjs index 818e0d95c8..f6e8b8405d 100644 --- a/client-sdk/ts-web/core/playground/src/startPlayground.mjs +++ b/client-sdk/ts-web/core/playground/src/startPlayground.mjs @@ -35,8 +35,8 @@ export async function startPlayground() { // Try sending a transaction. { - const src = oasis.signature.NaclSigner.fromRandom('this key is not important'); - const dst = oasis.signature.NaclSigner.fromRandom('this key is not important'); + const src = await oasis.signature.WebCryptoSigner.generate(false); + const dst = await oasis.signature.WebCryptoSigner.generate(false); console.log('src', src, 'dst', dst); const chainContext = await nic.consensusGetChainContext(); diff --git a/client-sdk/ts-web/ext-utils/sample-page/src/index.js b/client-sdk/ts-web/ext-utils/sample-page/src/index.js index eb1b10b5fa..a7daedb324 100644 --- a/client-sdk/ts-web/ext-utils/sample-page/src/index.js +++ b/client-sdk/ts-web/ext-utils/sample-page/src/index.js @@ -47,7 +47,7 @@ export const playground = (async function () { oasis.staking.addressToBech32(oasis.staking.addressFromPublicKey(publicKey)), ); - const dst = oasis.signature.NaclSigner.fromRandom('this key is not important'); + const dst = await oasis.signature.WebCryptoSigner.generate(false); const tw = oasis.staking .transferWrapper() .setNonce(101n) diff --git a/client-sdk/ts-web/rt/playground/src/consensus.js b/client-sdk/ts-web/rt/playground/src/consensus.js index c6b7657e8b..4303183b4a 100644 --- a/client-sdk/ts-web/rt/playground/src/consensus.js +++ b/client-sdk/ts-web/rt/playground/src/consensus.js @@ -119,9 +119,8 @@ export const playground = (async function () { })(); }); - const alice = oasis.signature.NaclSigner.fromSeed( + const alice = await oasis.signature.WebCryptoSigner.fromPrivateKey( oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: alice')), - 'this key is not important', ); const csAlice = new oasis.signature.BlindContextSigner(alice); const aliceAddr = oasis.staking.addressFromPublicKey(alice.public()); diff --git a/client-sdk/ts-web/rt/playground/src/index.js b/client-sdk/ts-web/rt/playground/src/index.js index 96be6baeed..3a01d2220c 100644 --- a/client-sdk/ts-web/rt/playground/src/index.js +++ b/client-sdk/ts-web/rt/playground/src/index.js @@ -145,14 +145,12 @@ export const playground = (async function () { console.log(`ready ${waitEnd2 - waitStart2} ms`); } - const alice = oasis.signature.NaclSigner.fromSeed( + const alice = await oasis.signature.WebCryptoSigner.fromPrivateKey( oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: alice')), - 'this key is not important', ); const csAlice = new oasis.signature.BlindContextSigner(alice); - const bob = oasis.signature.NaclSigner.fromSeed( + const bob = await oasis.signature.WebCryptoSigner.fromPrivateKey( oasis.hash.hash(oasis.misc.fromString('oasis-runtime-sdk/test-keys: bob')), - 'this key is not important', ); const csBob = new oasis.signature.BlindContextSigner(bob); diff --git a/client-sdk/ts-web/signer-ledger/playground/src/index.js b/client-sdk/ts-web/signer-ledger/playground/src/index.js index e1fbb929c2..16a918b92a 100644 --- a/client-sdk/ts-web/signer-ledger/playground/src/index.js +++ b/client-sdk/ts-web/signer-ledger/playground/src/index.js @@ -9,7 +9,7 @@ async function play() { // Try Ledger signing. { - const dst = oasis.signature.NaclSigner.fromRandom('this key is not important'); + const dst = await oasis.signature.WebCryptoSigner.generate(false); const dstAddr = await oasis.staking.addressFromPublicKey(dst.public()); console.log('dst addr', oasis.staking.addressToBech32(dstAddr)); From 92aa3a6e47e720daf2f5bbd0a0c3bd9184a3b655 Mon Sep 17 00:00:00 2001 From: Warren He Date: Thu, 10 Oct 2024 17:29:15 -0700 Subject: [PATCH 09/20] ts-web/signer-ledger: update derivation path reference --- client-sdk/ts-web/signer-ledger/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client-sdk/ts-web/signer-ledger/src/index.ts b/client-sdk/ts-web/signer-ledger/src/index.ts index 5a8cc84d31..5c5cce9718 100644 --- a/client-sdk/ts-web/signer-ledger/src/index.ts +++ b/client-sdk/ts-web/signer-ledger/src/index.ts @@ -61,7 +61,8 @@ export class LedgerContextSigner implements oasis.signature.ContextSigner { static async fromTransport(transport: Transport, keyNumber: number) { const app = new OasisApp(transport); - // Specification forthcoming. See https://github.com/oasisprotocol/oasis-core/pull/3656. + // Ledger clients use the "legacy" derivation path by default. + // https://github.com/oasisprotocol/cli/blob/v0.1.0/wallet/ledger/common.go#L15 const path = [44, 474, 0, 0, keyNumber]; const publicKeyResponse = successOrThrow(await app.publicKey(path), 'ledger public key'); return new LedgerContextSigner(app, path, u8FromBuf(publicKeyResponse.pk as Buffer)); From ba7bb1fbb56e732bfa4de946601af2f423e9407c Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 14:06:12 -0700 Subject: [PATCH 10/20] ts-web/core: misc add fromBase64url --- client-sdk/ts-web/core/src/misc.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client-sdk/ts-web/core/src/misc.ts b/client-sdk/ts-web/core/src/misc.ts index 1a98d43aa5..a72a8fc45e 100644 --- a/client-sdk/ts-web/core/src/misc.ts +++ b/client-sdk/ts-web/core/src/misc.ts @@ -82,6 +82,12 @@ export function fromBase64(base64: string) { return u8; } +export function fromBase64url(base64url: string) { + const padding = ['', '', '==', '='][base64url.length % 4]; + const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/') + padding; + return fromBase64(base64); +} + export function toStringUTF8(u8: Uint8Array) { return new TextDecoder().decode(u8); } From bd99f8530cbd47c850d97aff9848c4c8ba8ff159 Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 16:06:00 -0700 Subject: [PATCH 11/20] ts-web/signer-tweetnacl: remove note param --- .../ts-web/signer-tweetnacl/src/index.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/client-sdk/ts-web/signer-tweetnacl/src/index.ts b/client-sdk/ts-web/signer-tweetnacl/src/index.ts index 244d77c89c..55b26bb944 100644 --- a/client-sdk/ts-web/signer-tweetnacl/src/index.ts +++ b/client-sdk/ts-web/signer-tweetnacl/src/index.ts @@ -3,47 +3,43 @@ import * as nacl from 'tweetnacl'; import * as oasis from '@oasisprotocol/client'; /** - * An in-memory signer based on tweetnacl. We've included this for development. + * An in-memory signer based on tweetnacl. */ export class NaclSigner implements oasis.signature.Signer { key: nacl.SignKeyPair; - constructor(key: nacl.SignKeyPair, note: string) { - if (note !== 'this key is not important') throw new Error('insecure signer implementation'); + constructor(key: nacl.SignKeyPair) { this.key = key; } /** * Generate a keypair from a random seed - * @param note Set to 'this key is not important' to acknowledge the risks * @returns Instance of NaclSigner */ - static fromRandom(note: string) { + static fromRandom() { const secret = new Uint8Array(32); crypto.getRandomValues(secret); - return NaclSigner.fromSeed(secret, note); + return NaclSigner.fromSeed(secret); } /** * Instanciate from a given secret * @param secret 64 bytes ed25519 secret (h) that will be used to sign messages - * @param note Set to 'this key is not important' to acknowledge the risks * @returns Instance of NaclSigner */ - static fromSecret(secret: Uint8Array, note: string) { + static fromSecret(secret: Uint8Array) { const key = nacl.sign.keyPair.fromSecretKey(secret); - return new NaclSigner(key, note); + return new NaclSigner(key); } /** * Instanciate from a given seed * @param seed 32 bytes ed25519 seed (k) that will deterministically generate a private key - * @param note Set to 'this key is not important' to acknowledge the risks * @returns Instance of NaclSigner */ - static fromSeed(seed: Uint8Array, note: string) { + static fromSeed(seed: Uint8Array) { const key = nacl.sign.keyPair.fromSeed(seed); - return new NaclSigner(key, note); + return new NaclSigner(key); } /** From 5c93184e44eca7adba909248202bc75889daaa42 Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 16:25:18 -0700 Subject: [PATCH 12/20] ts-web/signer-tweetnacl: other comment changes --- client-sdk/ts-web/signer-tweetnacl/src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client-sdk/ts-web/signer-tweetnacl/src/index.ts b/client-sdk/ts-web/signer-tweetnacl/src/index.ts index 55b26bb944..920333c682 100644 --- a/client-sdk/ts-web/signer-tweetnacl/src/index.ts +++ b/client-sdk/ts-web/signer-tweetnacl/src/index.ts @@ -13,7 +13,7 @@ export class NaclSigner implements oasis.signature.Signer { } /** - * Generate a keypair from a random seed + * Generates a keypair from a random seed. * @returns Instance of NaclSigner */ static fromRandom() { @@ -23,8 +23,8 @@ export class NaclSigner implements oasis.signature.Signer { } /** - * Instanciate from a given secret - * @param secret 64 bytes ed25519 secret (h) that will be used to sign messages + * Instantiates from a given 64-bite `nacl.sign` secret key. + * @param secret Secret key * @returns Instance of NaclSigner */ static fromSecret(secret: Uint8Array) { @@ -33,8 +33,8 @@ export class NaclSigner implements oasis.signature.Signer { } /** - * Instanciate from a given seed - * @param seed 32 bytes ed25519 seed (k) that will deterministically generate a private key + * Instantiates from a given 32-byte `nacl.sign` seed. + * @param seed Seed * @returns Instance of NaclSigner */ static fromSeed(seed: Uint8Array) { @@ -43,7 +43,7 @@ export class NaclSigner implements oasis.signature.Signer { } /** - * Returns the 32 bytes public key of this key pair + * Returns the 32-byte public key of this key pair. * @returns Public key */ public(): Uint8Array { @@ -51,7 +51,7 @@ export class NaclSigner implements oasis.signature.Signer { } /** - * Signs the given message + * Signs the given message. * @param message Bytes to sign * @returns Signed message */ From 7a6490151c13e27e0d88b32844c620e22783758a Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 16:34:51 -0700 Subject: [PATCH 13/20] ts-web/core: hdkey remove unused import --- client-sdk/ts-web/core/src/hdkey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-sdk/ts-web/core/src/hdkey.ts b/client-sdk/ts-web/core/src/hdkey.ts index 0e37c5e0c0..1250a10af8 100644 --- a/client-sdk/ts-web/core/src/hdkey.ts +++ b/client-sdk/ts-web/core/src/hdkey.ts @@ -1,7 +1,7 @@ import {hmac} from '@noble/hashes/hmac'; import {sha512} from '@noble/hashes/sha512'; import {SignKeyPair, sign} from 'tweetnacl'; -import {generateMnemonic, mnemonicToSeed, validateMnemonic} from 'bip39'; +import {generateMnemonic, mnemonicToSeed} from 'bip39'; import {concat} from './misc'; const ED25519_CURVE = 'ed25519 seed'; From 9a1b4bcc9b9b15dfd8ec7e9a0919926c216513a3 Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 17:37:34 -0700 Subject: [PATCH 14/20] ts-web/core: hdkey remove tweetnacl specific types --- client-sdk/ts-web/core/src/hdkey.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/client-sdk/ts-web/core/src/hdkey.ts b/client-sdk/ts-web/core/src/hdkey.ts index 1250a10af8..c1c84b2fc1 100644 --- a/client-sdk/ts-web/core/src/hdkey.ts +++ b/client-sdk/ts-web/core/src/hdkey.ts @@ -1,6 +1,5 @@ import {hmac} from '@noble/hashes/hmac'; import {sha512} from '@noble/hashes/sha512'; -import {SignKeyPair, sign} from 'tweetnacl'; import {generateMnemonic, mnemonicToSeed} from 'bip39'; import {concat} from './misc'; @@ -13,27 +12,25 @@ const pathRegex = new RegExp("^m(\\/[0-9]+')+$"); * https://github.com/oasisprotocol/adrs/blob/main/0008-standard-account-key-generation.md */ export class HDKey { - public readonly keypair: SignKeyPair; - /** * Generates the keypair matching the supplied parameters * @param mnemonic BIP-0039 Mnemonic * @param index Account index * @param passphrase Optional BIP-0039 passphrase - * @returns SignKeyPair for these parameters + * @returns ed25519 private key for these parameters */ public static async getAccountSigner( mnemonic: string, index: number = 0, passphrase?: string, - ): Promise { + ): Promise { if (index < 0 || index > 0x7fffffff) { throw new Error('Account number must be >= 0 and <= 2147483647'); } const seed = await mnemonicToSeed(mnemonic, passphrase); const key = HDKey.makeHDKey(ED25519_CURVE, seed); - return key.derivePath(`m/44'/474'/${index}'`).keypair; + return key.derivePath(`m/44'/474'/${index}'`).privateKey; } /** @@ -48,9 +45,7 @@ export class HDKey { private constructor( private readonly privateKey: Uint8Array, private readonly chainCode: Uint8Array, - ) { - this.keypair = sign.keyPair.fromSeed(privateKey); - } + ) {} /** * Returns the HDKey for the given derivation path From 26ef079b098441f1b914ed0a0900de15af8752a4 Mon Sep 17 00:00:00 2001 From: Warren He Date: Fri, 11 Oct 2024 19:27:36 -0700 Subject: [PATCH 15/20] ts-web/core: hdkey getAccountSigher actually return a signer also split out seedFromMnemonic and privateKeyFromSeed --- client-sdk/ts-web/core/src/hdkey.ts | 50 ++++++++++++++++++----- client-sdk/ts-web/core/test/hdkey.test.ts | 17 ++++---- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/client-sdk/ts-web/core/src/hdkey.ts b/client-sdk/ts-web/core/src/hdkey.ts index c1c84b2fc1..82d51b70c9 100644 --- a/client-sdk/ts-web/core/src/hdkey.ts +++ b/client-sdk/ts-web/core/src/hdkey.ts @@ -2,6 +2,7 @@ import {hmac} from '@noble/hashes/hmac'; import {sha512} from '@noble/hashes/sha512'; import {generateMnemonic, mnemonicToSeed} from 'bip39'; import {concat} from './misc'; +import {Signer, WebCryptoSigner} from './signature'; const ED25519_CURVE = 'ed25519 seed'; const HARDENED_OFFSET = 0x80000000; @@ -12,25 +13,54 @@ const pathRegex = new RegExp("^m(\\/[0-9]+')+$"); * https://github.com/oasisprotocol/adrs/blob/main/0008-standard-account-key-generation.md */ export class HDKey { + private static ensureValidIndex(index: number) { + if (index < 0 || index > 0x7fffffff) { + throw new Error('Account number must be >= 0 and <= 2147483647'); + } + } + /** - * Generates the keypair matching the supplied parameters - * @param mnemonic BIP-0039 Mnemonic - * @param index Account index + * Generates the seed matching the supplied parameters + * @param mnemonic BIP-0039 mnemonic * @param passphrase Optional BIP-0039 passphrase + * @returns BIP-0039 seed + */ + public static async seedFromMnemonic(mnemonic: string, passphrase?: string) { + return new Uint8Array(await mnemonicToSeed(mnemonic, passphrase)); + } + + /** + * Generates the signer matching the supplied parameters + * @param seed BIP-0039 seed + * @param index Account index * @returns ed25519 private key for these parameters */ + public static privateKeyFromSeed(seed: Uint8Array, index: number = 0) { + HDKey.ensureValidIndex(index); + + const key = HDKey.makeHDKey(ED25519_CURVE, seed); + return key.derivePath(`m/44'/474'/${index}'`).privateKey; + } + + /** + * Generates the Signer matching the supplied parameters + * @param mnemonic BIP-0039 mnemonic + * @param index Account index + * @param passphrase Optional BIP-0039 passphrase + * @returns Signer for these parameters + */ public static async getAccountSigner( mnemonic: string, index: number = 0, passphrase?: string, - ): Promise { - if (index < 0 || index > 0x7fffffff) { - throw new Error('Account number must be >= 0 and <= 2147483647'); - } + ): Promise { + // privateKeyFromSeed checks too, but validate before the expensive + // seedFromMnemonic call. + HDKey.ensureValidIndex(index); - const seed = await mnemonicToSeed(mnemonic, passphrase); - const key = HDKey.makeHDKey(ED25519_CURVE, seed); - return key.derivePath(`m/44'/474'/${index}'`).privateKey; + const seed = await HDKey.seedFromMnemonic(mnemonic, passphrase); + const privateKey = HDKey.privateKeyFromSeed(seed, index); + return await WebCryptoSigner.fromPrivateKey(privateKey); } /** diff --git a/client-sdk/ts-web/core/test/hdkey.test.ts b/client-sdk/ts-web/core/test/hdkey.test.ts index d1614f8b89..5baf60f0bd 100644 --- a/client-sdk/ts-web/core/test/hdkey.test.ts +++ b/client-sdk/ts-web/core/test/hdkey.test.ts @@ -1,4 +1,6 @@ import {HDKey} from '../src/hdkey'; +import {concat, toHex} from '../src/misc'; +import {WebCryptoSigner} from '../src/signature'; import * as adr0008VectorsRaw from './adr-0008-vectors.json'; interface Adr0008Vector { @@ -16,8 +18,6 @@ interface Adr0008Vector { const adr0008Vectors: Adr0008Vector[] = adr0008VectorsRaw; -const uint2hex = (array: Uint8Array) => Buffer.from(array).toString('hex'); - describe('HDKey', () => { describe('getAccountSigner', () => { it('Should reject negative account numbers', async () => { @@ -41,17 +41,16 @@ describe('HDKey', () => { ? vector.bip39_passphrase : undefined; + const seed = await HDKey.seedFromMnemonic(vector.bip39_mnemonic, passphrase); for (let account of vector.oasis_accounts) { expect(account.bip32_path).toMatch(/^m\/44'\/474'\/[0-9]+'/); const index = Number(account.bip32_path.split('/').pop()!.replace("'", '')); - const keyPair = await HDKey.getAccountSigner( - vector.bip39_mnemonic, - index, - passphrase, - ); + const privateKey = HDKey.privateKeyFromSeed(seed, index); + const signer = await WebCryptoSigner.fromPrivateKey(privateKey); - expect(uint2hex(keyPair.secretKey)).toEqual(account.private_key); - expect(uint2hex(keyPair.publicKey)).toEqual(account.public_key); + const publicKey = signer.public(); + expect(toHex(concat(privateKey, publicKey))).toEqual(account.private_key); + expect(toHex(publicKey)).toEqual(account.public_key); } }); }); From ace92804865183acfcc96e694a40bef089932a17 Mon Sep 17 00:00:00 2001 From: Warren He Date: Mon, 14 Oct 2024 13:18:57 -0700 Subject: [PATCH 16/20] ts-web/ext-utils: sample-ext use new hdkey API --- client-sdk/ts-web/ext-utils/sample-ext/src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client-sdk/ts-web/ext-utils/sample-ext/src/index.js b/client-sdk/ts-web/ext-utils/sample-ext/src/index.js index a847031e80..b117459826 100644 --- a/client-sdk/ts-web/ext-utils/sample-ext/src/index.js +++ b/client-sdk/ts-web/ext-utils/sample-ext/src/index.js @@ -152,8 +152,7 @@ function getSigner() { ); } } - const pair = await oasis.hdkey.HDKey.getAccountSigner(mnemonic); - const rawSigner = new oasis.signature.NaclSigner(pair, 'this key is not important'); + const rawSigner = await oasis.hdkey.HDKey.getAccountSigner(mnemonic); return new oasis.signature.BlindContextSigner(rawSigner); })(); } From 9b47f2ff58e8af0319d8ddbf9d0e931e8729164a Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 15 Oct 2024 13:37:02 -0700 Subject: [PATCH 17/20] ts-web/core: remove tweetnacl dependency --- client-sdk/ts-web/core/package.json | 3 +-- client-sdk/ts-web/package-lock.json | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/client-sdk/ts-web/core/package.json b/client-sdk/ts-web/core/package.json index 5e013d1248..af7928566d 100644 --- a/client-sdk/ts-web/core/package.json +++ b/client-sdk/ts-web/core/package.json @@ -30,8 +30,7 @@ "bip39": "^3.1.0", "cborg": "^2.0.3", "grpc-web": "^1.5.0", - "protobufjs": "~7.4.0", - "tweetnacl": "^1.0.3" + "protobufjs": "~7.4.0" }, "devDependencies": { "@types/jest": "^29.5.13", diff --git a/client-sdk/ts-web/package-lock.json b/client-sdk/ts-web/package-lock.json index 90c8ccbcd8..07916a8b09 100644 --- a/client-sdk/ts-web/package-lock.json +++ b/client-sdk/ts-web/package-lock.json @@ -22,8 +22,7 @@ "bip39": "^3.1.0", "cborg": "^2.0.3", "grpc-web": "^1.5.0", - "protobufjs": "~7.4.0", - "tweetnacl": "^1.0.3" + "protobufjs": "~7.4.0" }, "devDependencies": { "@types/jest": "^29.5.13", @@ -10500,7 +10499,6 @@ "protobufjs-cli": "^1.1.3", "stream-browserify": "^3.0.0", "ts-jest": "^29.2.5", - "tweetnacl": "^1.0.3", "typedoc": "^0.26.7", "typescript": "^5.6.2", "webpack": "^5.95.0", From 59e62cd813fedd8c02aa96c59c23868a541f16ec Mon Sep 17 00:00:00 2001 From: Warren He Date: Tue, 15 Oct 2024 13:37:30 -0700 Subject: [PATCH 18/20] ts-web/core: hdkey test set up global web crypto --- client-sdk/ts-web/core/test/hdkey.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client-sdk/ts-web/core/test/hdkey.test.ts b/client-sdk/ts-web/core/test/hdkey.test.ts index 5baf60f0bd..d3f63ffb36 100644 --- a/client-sdk/ts-web/core/test/hdkey.test.ts +++ b/client-sdk/ts-web/core/test/hdkey.test.ts @@ -1,8 +1,16 @@ +import {webcrypto} from 'crypto'; + import {HDKey} from '../src/hdkey'; import {concat, toHex} from '../src/misc'; import {WebCryptoSigner} from '../src/signature'; + import * as adr0008VectorsRaw from './adr-0008-vectors.json'; +if (typeof crypto === 'undefined') { + // @ts-expect-error there are some inconsequential type differences + globalThis.crypto = webcrypto; +} + interface Adr0008Vector { kind: string; bip39_mnemonic: string; From cc0781456e1bd35e30a640bd2cd1328597d377e8 Mon Sep 17 00:00:00 2001 From: Warren He Date: Mon, 14 Oct 2024 17:05:56 -0700 Subject: [PATCH 19/20] ts-web/core: add changelog --- client-sdk/ts-web/core/docs/changelog.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client-sdk/ts-web/core/docs/changelog.md b/client-sdk/ts-web/core/docs/changelog.md index 6f7624318a..c85c748201 100644 --- a/client-sdk/ts-web/core/docs/changelog.md +++ b/client-sdk/ts-web/core/docs/changelog.md @@ -2,6 +2,15 @@ ## Unreleased changes +Breaking changes: + +- `signature.NaclSigner` is moved out to a new + `@oasisprotocol/signer-tweetnacl` package. +- `hdkey.HDKey.getAccountSigner` now returns a `signature.Signer` instead of + a tweetnacl `SignKeyPair`. + To get the private key, use the new `hdkey.HDKey.seedFromMnemonic` and + `hdkey.HDKey.privateKeyFromSeed` functions. + New features: - Hashing and many related functions that internally need to compute a hash, @@ -10,11 +19,19 @@ New features: We had implementations that used synchronous hashing libraries all along, but this is us giving up on eventually using the Web Crypto API for SHA-512/256. +- For Ed25519 signing, there's a new `signature.WebCryptoSigner` taking the + place of `signature.NaclSigner`. + `await signature.WebCryptoSigner.generate(extractable)` is equivalent to + `signature.NaclSigner.fromRandom(note)`, and + `await signature.WebCryptoSigner.fromPrivateKey(priv)` is equivalent to + `signature.NaclSigner.fromSeed(priv, note)`. Little things: +- Removed dependency on tweetnacl. - We're switching lots of cryptography dependencies to noble cryptography libraries. +- Ed25519 verification now uses the Web Crypto API. ## v1.1.0 From 60ecfae990b0b4b89d6ccb7350580d7138b1049f Mon Sep 17 00:00:00 2001 From: Warren He Date: Wed, 16 Oct 2024 20:47:03 -0700 Subject: [PATCH 20/20] ts-web/signer-tweetnacl: add changelog --- .../ts-web/signer-tweetnacl/docs/changelog.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 client-sdk/ts-web/signer-tweetnacl/docs/changelog.md diff --git a/client-sdk/ts-web/signer-tweetnacl/docs/changelog.md b/client-sdk/ts-web/signer-tweetnacl/docs/changelog.md new file mode 100644 index 0000000000..1a35e721e8 --- /dev/null +++ b/client-sdk/ts-web/signer-tweetnacl/docs/changelog.md @@ -0,0 +1,14 @@ +# Changelog + +## Unreleased changes + +New features: + +- This is the `NaclSigner` class formerly in `@oasisprotocol/client`. Feel + free to continue using it for development. + +Breaking changes: + +- The `note` parameter is removed. Our opinion on using in-application-memory + keys is unchanged. But if you're taking the step of installing this library + called "signer-tweetnacl," you that it's using tweetnacl under the hood.