From 230f47d27eba7c4b48a076a89dde1b8042fa2723 Mon Sep 17 00:00:00 2001 From: Akim <59872966+akim-bow@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:26:44 +0700 Subject: [PATCH] fix(chore): Additional test for particle signature (#356) * Additional test for particle signature * remove .only * Fix bs58 * Fix bs58 #2 * update test * update test * fix test * remove only * refactor * refactor --------- Co-authored-by: Aleksey Proshutisnkiy Co-authored-by: Alexey Proshutinskiy --- .../src/connection/RelayConnection.ts | 6 +- .../src/keypair/__test__/KeyPair.spec.ts | 154 +++++++++-------- packages/core/js-client/src/keypair/index.ts | 155 ++++++++++-------- .../core/js-client/src/particle/Particle.ts | 150 ++++++++++------- packages/core/js-client/src/util/bytes.ts | 6 +- 5 files changed, 270 insertions(+), 201 deletions(-) diff --git a/packages/core/js-client/src/connection/RelayConnection.ts b/packages/core/js-client/src/connection/RelayConnection.ts index 774a1677..6ec6833d 100644 --- a/packages/core/js-client/src/connection/RelayConnection.ts +++ b/packages/core/js-client/src/connection/RelayConnection.ts @@ -34,7 +34,7 @@ import { Subject } from 'rxjs'; import { throwIfHasNoPeerId } from '../util/libp2pUtils.js'; import { IConnection } from './interfaces.js'; import { IParticle } from '../particle/interfaces.js'; -import { Particle, serializeToString, verifySignature } from '../particle/Particle.js'; +import { buildParticleMessage, Particle, serializeToString, verifySignature } from '../particle/Particle.js'; import { identifyService } from 'libp2p/identify'; import { pingService } from 'libp2p/ping'; import { unmarshalPublicKey } from '@libp2p/crypto/keys'; @@ -186,7 +186,9 @@ export class RelayConnection implements IConnection { return; } - const isVerified = await verifySignature(particle, initPeerId.publicKey); + // TODO: uncomment this after nox rolls out signature verification + // const isVerified = await KeyPair.verifyWithPublicKey(initPeerId.publicKey, buildParticleMessage(particle), particle.signature); + const isVerified = true; if (isVerified) { this.particleSource.next(particle); } else { diff --git a/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts b/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts index 79961b02..bda57e39 100644 --- a/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts +++ b/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts @@ -1,4 +1,4 @@ -/* +/** * Copyright 2023 Fluence Labs Limited * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { it, describe, expect } from 'vitest'; -import { toUint8Array } from 'js-base64'; -import * as bs58 from 'bs58'; -import { KeyPair } from '../index.js'; -// @ts-ignore -const { decode } = bs58.default; +import bs58 from "bs58"; +import { fromUint8Array, toUint8Array } from 'js-base64'; +import { it, describe, expect } from "vitest"; +import { fromBase64Sk, KeyPair } from '../index.js'; -const key = '+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk='; +import { Particle, serializeToString, buildParticleMessage } from '../../particle/Particle.js'; + +const key = "+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk="; const keyBytes = toUint8Array(key); const testData = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9, 10]); @@ -34,76 +34,102 @@ const testDataSig = Uint8Array.from([ // signature produced by KeyPair created from some random KeyPair -describe('KeyPair tests', () => { - it('generate keypair from seed', async function () { - // arrange - const random = await KeyPair.randomEd25519(); - const privateKey = random.toEd25519PrivateKey(); +describe("KeyPair tests", () => { + it("generate keypair from seed", async function () { + // arrange + const random = await KeyPair.randomEd25519(); + const privateKey = random.toEd25519PrivateKey(); - // act - const keyPair = await KeyPair.fromEd25519SK(privateKey); - const privateKey2 = keyPair.toEd25519PrivateKey(); + // act + const keyPair = await KeyPair.fromEd25519SK(privateKey); + const privateKey2 = keyPair.toEd25519PrivateKey(); - // assert - expect(privateKey).toStrictEqual(privateKey2); - }); + // assert + expect(privateKey).toStrictEqual(privateKey2); + }); - it('create keypair from ed25519 private key', async function () { - // arrange - const rustSK = 'jDaxLJzYtzgwTMrELJCAqavtmx85ktQNfB2rLcK7MhH'; - const sk = decode(rustSK); + it("create keypair from ed25519 private key", async function () { + // arrange + const rustSK = "jDaxLJzYtzgwTMrELJCAqavtmx85ktQNfB2rLcK7MhH"; + const sk = bs58.decode(rustSK); - // act - const keyPair = await KeyPair.fromEd25519SK(sk); + // act + const keyPair = await KeyPair.fromEd25519SK(sk); - // assert - const expectedPeerId = '12D3KooWH1W3VznVZ87JH4FwABK4mkntcspTVWJDta6c2xg9Pzbp'; - expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId); - }); + // assert + const expectedPeerId = + "12D3KooWH1W3VznVZ87JH4FwABK4mkntcspTVWJDta6c2xg9Pzbp"; - it('create keypair from a seed phrase', async function () { - // arrange - const seedArray = new Uint8Array(32).fill(1); + expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId); + }); - // act - const keyPair = await KeyPair.fromEd25519SK(seedArray); + it("create keypair from a seed phrase", async function () { + // arrange + const seedArray = new Uint8Array(32).fill(1); - // assert - const expectedPeerId = '12D3KooWK99VoVxNE7XzyBwXEzW7xhK7Gpv85r9F3V3fyKSUKPH5'; - expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId); - }); + // act + const keyPair = await KeyPair.fromEd25519SK(seedArray); - it('sign', async function () { - // arrange - const keyPair = await KeyPair.fromEd25519SK(keyBytes); + // assert + const expectedPeerId = + "12D3KooWK99VoVxNE7XzyBwXEzW7xhK7Gpv85r9F3V3fyKSUKPH5"; - // act - const res = await keyPair.signBytes(testData); - // assert - expect(new Uint8Array(res)).toStrictEqual(testDataSig); - }); + expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId); + }); - it('verify', async function () { - // arrange - const keyPair = await KeyPair.fromEd25519SK(keyBytes); + it("sign", async function () { + // arrange + const keyPair = await KeyPair.fromEd25519SK(keyBytes); - // act - const res = await keyPair.verify(testData, testDataSig); + // act + const res = await keyPair.signBytes(testData); + // assert + expect(new Uint8Array(res)).toStrictEqual(testDataSig); + }); - // assert - expect(res).toBe(true); - }); + it("verify", async function () { + // arrange + const keyPair = await KeyPair.fromEd25519SK(keyBytes); - it('sign-verify', async function () { - // arrange - const keyPair = await KeyPair.fromEd25519SK(keyBytes); + // act + const res = await keyPair.verify(testData, testDataSig); - // act - const data = new Uint8Array(32).fill(1); - const sig = await keyPair.signBytes(data); - const res = await keyPair.verify(data, sig); + // assert + expect(res).toBe(true); + }); - // assert - expect(res).toBe(true); - }); + it("sign-verify", async function () { + // arrange + const keyPair = await KeyPair.fromEd25519SK(keyBytes); + + // act + const data = new Uint8Array(32).fill(1); + const sig = await keyPair.signBytes(data); + const res = await keyPair.verify(data, sig); + + // assert + expect(res).toBe(true); + }); + + it("validates particle signature checks", async function () { + const keyPair = await fromBase64Sk("7h48PQ/f1rS9TxacmgODxbD42Il9B3KC117jvOPppPE="); + expect(bs58.encode(keyPair.getLibp2pPeerId().toBytes())).toBe("12D3KooWANqfCDrV79MZdMnMqTvDdqSAPSxdgFY1L6DCq2DVGB4D"); + const message = toUint8Array(btoa("message")); + const signature = await keyPair.signBytes(message); + + const verified = await keyPair.verify(message, signature); + expect(verified).toBe(true); + expect(fromUint8Array(signature)).toBe("sBW7H6/1fwAwF86ldwVm9BDu0YH3w30oFQjTWX0Tiu9yTVZHmxkV2OX4GL5jn0Iz0CrasGcOfozzkZwtJBPMBg=="); + + const particle = await Particle.createNew("abc", keyPair.getPeerId(), 7000, keyPair, "2883f959-e9e7-4843-8c37-205d393ca372", 1696934545662); + + const particle_bytes = buildParticleMessage(particle); + expect(fromUint8Array(particle_bytes)).toBe("Mjg4M2Y5NTktZTllNy00ODQzLThjMzctMjA1ZDM5M2NhMzcy/kguGYsBAABYGwAAYWJj"); + + const isParticleVerified = await KeyPair.verifyWithPublicKey(keyPair.getPublicKey(), particle_bytes, particle.signature); + + expect(isParticleVerified).toBe(true); + + expect(fromUint8Array(particle.signature)).toBe("KceXDnOfqe0dOnAxiDsyWBIvUq6WHoT0ge+VMHXOZsjZvCNH7/10oufdlYfcPomfv28On6E87ZhDcHGBZcb7Bw=="); + }); }); diff --git a/packages/core/js-client/src/keypair/index.ts b/packages/core/js-client/src/keypair/index.ts index a6ce6003..353488c3 100644 --- a/packages/core/js-client/src/keypair/index.ts +++ b/packages/core/js-client/src/keypair/index.ts @@ -1,5 +1,5 @@ -/* - * Copyright 2020 Fluence Labs Limited +/** + * Copyright 2023 Fluence Labs Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,92 +14,105 @@ * limitations under the License. */ -import type { PeerId } from '@libp2p/interface/peer-id'; -import { generateKeyPairFromSeed, generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys'; -import { createFromPrivKey, createFromPubKey } from '@libp2p/peer-id-factory'; -import type { PrivateKey, PublicKey } from '@libp2p/interface/keys'; -import { toUint8Array } from 'js-base64'; -import * as bs58 from 'bs58'; -import { KeyPairOptions } from '@fluencelabs/interfaces'; - -// @ts-ignore -const { decode } = bs58.default; +import { KeyPairOptions } from "@fluencelabs/interfaces"; +import { + generateKeyPairFromSeed, + generateKeyPair, + unmarshalPublicKey, +} from "@libp2p/crypto/keys"; +import type { PrivateKey, PublicKey } from "@libp2p/interface/keys"; +import type { PeerId } from "@libp2p/interface/peer-id"; +import { createFromPrivKey } from "@libp2p/peer-id-factory"; +import bs58 from "bs58"; +import { toUint8Array } from "js-base64"; export class KeyPair { - /** - * Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation - */ - getLibp2pPeerId() { - return this.libp2pPeerId; - } + private publicKey: PublicKey; - constructor( - private privateKey: PrivateKey | undefined, - private publicKey: PublicKey, - private libp2pPeerId: PeerId - ) {} + private constructor( + private privateKey: PrivateKey, + private libp2pPeerId: PeerId, + ) { + this.publicKey = privateKey.public; + } - /** - * Generates new KeyPair from ed25519 private key represented as a 32 byte array - * @param seed - Any sequence of 32 bytes - * @returns - Promise with the created KeyPair - */ - static async fromEd25519SK(seed: Uint8Array): Promise { - const key = await generateKeyPairFromSeed('Ed25519', seed, 256); - const lib2p2Pid = await createFromPrivKey(key); - return new KeyPair(key, key.public, lib2p2Pid); - } + /** + * Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation + */ + getLibp2pPeerId() { + return this.libp2pPeerId; + } - /** - * Generates new KeyPair with a random secret key - * @returns - Promise with the created KeyPair - */ - static async randomEd25519(): Promise { - const key = await generateKeyPair('Ed25519'); - const lib2p2Pid = await createFromPrivKey(key); - return new KeyPair(key, key.public, lib2p2Pid); - } + /** + * Return public key inferred from private key + */ + getPublicKey() { + return this.publicKey.bytes; + } - getPeerId(): string { - return this.libp2pPeerId.toString(); - } + /** + * Generates new KeyPair from ed25519 private key represented as a 32 byte array + * @param seed - Any sequence of 32 bytes + * @returns - Promise with the created KeyPair + */ + static async fromEd25519SK(seed: Uint8Array): Promise { + const key = await generateKeyPairFromSeed("Ed25519", seed, 256); + const lib2p2Pid = await createFromPrivKey(key); + return new KeyPair(key, lib2p2Pid); + } - /** - * @returns 32 byte private key - */ - toEd25519PrivateKey(): Uint8Array { - if (this.privateKey === undefined) { - throw new Error('Private key not supplied'); - } - return this.privateKey.marshal().subarray(0, 32); - } + /** + * Generates new KeyPair with a random secret key + * @returns - Promise with the created KeyPair + */ + static async randomEd25519(): Promise { + const key = await generateKeyPair("Ed25519"); + const lib2p2Pid = await createFromPrivKey(key); + return new KeyPair(key, lib2p2Pid); + } - signBytes(data: Uint8Array): Promise { - if (this.privateKey === undefined) { - throw new Error('Private key not supplied'); - } - return this.privateKey.sign(data); - } + static verifyWithPublicKey( + publicKey: Uint8Array, + message: Uint8Array, + signature: Uint8Array, + ) { + return unmarshalPublicKey(publicKey).verify(message, signature); + } - verify(data: Uint8Array, signature: Uint8Array): Promise { - return this.publicKey.verify(data, signature); - } + getPeerId(): string { + return this.libp2pPeerId.toString(); + } + + /** + * @returns 32 byte private key + */ + toEd25519PrivateKey(): Uint8Array { + return this.privateKey.marshal().subarray(0, 32); + } + + signBytes(data: Uint8Array): Promise { + return this.privateKey.sign(data); + } + + verify(data: Uint8Array, signature: Uint8Array): Promise { + return this.publicKey.verify(data, signature); + } } export const fromBase64Sk = (sk: string): Promise => { - const skArr = toUint8Array(sk); - return KeyPair.fromEd25519SK(skArr); + const skArr = toUint8Array(sk); + return KeyPair.fromEd25519SK(skArr); }; export const fromBase58Sk = (sk: string): Promise => { - const skArr = decode(sk); - return KeyPair.fromEd25519SK(skArr); + const skArr = bs58.decode(sk); + return KeyPair.fromEd25519SK(skArr); }; export const fromOpts = (opts: KeyPairOptions): Promise => { - if (opts.source === 'random') { - return KeyPair.randomEd25519(); - } + if (opts.source === "random") { + return KeyPair.randomEd25519(); + } - return KeyPair.fromEd25519SK(opts.source); + return KeyPair.fromEd25519SK(opts.source); }; diff --git a/packages/core/js-client/src/particle/Particle.ts b/packages/core/js-client/src/particle/Particle.ts index 5482ca18..7c5277d5 100644 --- a/packages/core/js-client/src/particle/Particle.ts +++ b/packages/core/js-client/src/particle/Particle.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { atob, fromUint8Array, toUint8Array } from 'js-base64'; -import { CallResultsArray } from '@fluencelabs/avm'; -import { v4 as uuidv4 } from 'uuid'; -import { Buffer } from 'buffer'; -import { IParticle } from './interfaces.js'; -import { concat } from 'uint8arrays/concat'; -import { numberToLittleEndianBytes } from '../util/bytes.js'; -import { KeyPair } from '../keypair/index.js'; -import { unmarshalPublicKey } from '@libp2p/crypto/keys'; +import { CallResultsArray } from "@fluencelabs/avm"; +import { fromUint8Array, toUint8Array } from "js-base64"; +import { concat } from "uint8arrays/concat"; +import { v4 as uuidv4 } from "uuid"; + +import { KeyPair } from "../keypair/index.js"; +import { numberToLittleEndianBytes } from "../util/bytes.js"; + +import { IParticle } from "./interfaces.js"; export class Particle implements IParticle { constructor( @@ -34,14 +34,31 @@ export class Particle implements IParticle { public readonly initPeerId: string, public readonly signature: Uint8Array ) {} - - static async createNew(script: string, initPeerId: string, ttl: number, keyPair: KeyPair): Promise { - const id = uuidv4(); - const timestamp = Date.now(); - const message = buildParticleMessage({ id, timestamp, ttl, script }); - const signature = await keyPair.signBytes(message); - return new Particle(id, Date.now(), script, Buffer.from([]), ttl, initPeerId, signature); - } + + static async createNew( + script: string, + initPeerId: string, + ttl: number, + keyPair: KeyPair, + _id?: string, + _timestamp?: number, + _data?: Uint8Array, + ): Promise { + const id = _id ?? uuidv4(); + const timestamp = _timestamp ?? Date.now(); + const data = _data ?? new Uint8Array([]); + const message = buildParticleMessage({ id, timestamp, ttl, script }); + const signature = await keyPair.signBytes(message); + return new Particle( + id, + timestamp, + script, + data, + ttl, + initPeerId, + signature, + ); + } static fromString(str: string): Particle { const json = JSON.parse(str); @@ -64,27 +81,32 @@ const en = new TextEncoder(); /** * Builds particle message for signing */ -export const buildParticleMessage = ({ id, timestamp, ttl, script }: Omit): Uint8Array => { - return concat([ - en.encode(id), - numberToLittleEndianBytes(timestamp, 'u64'), - numberToLittleEndianBytes(ttl, 'u32'), - en.encode(script), - ]); -} +export const buildParticleMessage = ({ + id, + timestamp, + ttl, + script, +}: Omit): Uint8Array => { + return concat([ + en.encode(id), + numberToLittleEndianBytes(timestamp, "u64"), + numberToLittleEndianBytes(ttl, "u32"), + en.encode(script), + ]); +}; /** * Returns actual ttl of a particle, i.e. ttl - time passed since particle creation */ export const getActualTTL = (particle: IParticle): number => { - return particle.timestamp + particle.ttl - Date.now(); + return particle.timestamp + particle.ttl - Date.now(); }; /** * Returns true if particle has expired */ export const hasExpired = (particle: IParticle): boolean => { - return getActualTTL(particle) <= 0; + return getActualTTL(particle) <= 0; }; /** @@ -100,59 +122,65 @@ export const verifySignature = async (particle: IParticle, publicKey: Uint8Array /** * Creates a particle clone with new data */ -export const cloneWithNewData = (particle: IParticle, newData: Uint8Array): IParticle => { - return new Particle(particle.id, particle.timestamp, particle.script, newData, particle.ttl, particle.initPeerId, particle.signature); -}; - -/** - * Creates a deep copy of a particle - */ -export const fullClone = (particle: IParticle): IParticle => { - return JSON.parse(JSON.stringify(particle)); +export const cloneWithNewData = ( + particle: IParticle, + newData: Uint8Array, +): IParticle => { + return new Particle( + particle.id, + particle.timestamp, + particle.script, + newData, + particle.ttl, + particle.initPeerId, + particle.signature, + ); }; /** * Serializes particle into string suitable for sending through network */ export const serializeToString = (particle: IParticle): string => { - return JSON.stringify({ - action: 'Particle', - id: particle.id, - init_peer_id: particle.initPeerId, - timestamp: particle.timestamp, - ttl: particle.ttl, - script: particle.script, - signature: Array.from(particle.signature), - data: particle.data && fromUint8Array(particle.data), - }); + return JSON.stringify({ + action: "Particle", + id: particle.id, + init_peer_id: particle.initPeerId, + timestamp: particle.timestamp, + ttl: particle.ttl, + script: particle.script, + signature: Array.from(particle.signature), + data: fromUint8Array(particle.data), + }); }; /** * When particle is executed, it goes through different stages. The type describes all possible stages and their parameters */ export type ParticleExecutionStage = - | { stage: 'received' } - | { stage: 'interpreted' } - | { stage: 'interpreterError'; errorMessage: string } - | { stage: 'localWorkDone' } - | { stage: 'sent' } - | { stage: 'sendingError'; errorMessage: string } - | { stage: 'expired' }; + | { stage: "received" } + | { stage: "interpreted" } + | { stage: "interpreterError"; errorMessage: string } + | { stage: "localWorkDone" } + | { stage: "sent" } + | { stage: "sendingError"; errorMessage: string } + | { stage: "expired" }; /** * Particle queue item is a wrapper around particle, which contains additional information about particle execution */ export interface ParticleQueueItem { - particle: IParticle; - callResults: CallResultsArray; - onStageChange: (state: ParticleExecutionStage) => void; + particle: IParticle; + callResults: CallResultsArray; + onStageChange: (state: ParticleExecutionStage) => void; } /** * Helper function to handle particle at expired stage */ -export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => { - if (stage.stage === 'expired') { - fn(); - } -}; +export const handleTimeout = (fn: () => void) => { + return (stage: ParticleExecutionStage) => { + if (stage.stage === "expired") { + fn(); + } + } +} diff --git a/packages/core/js-client/src/util/bytes.ts b/packages/core/js-client/src/util/bytes.ts index 5e19461a..36c37f09 100644 --- a/packages/core/js-client/src/util/bytes.ts +++ b/packages/core/js-client/src/util/bytes.ts @@ -23,10 +23,10 @@ const sizeMap = { function numberToBytes(n: number, s: Size, littleEndian: boolean) { const size = sizeMap[s]; - const buffer = new ArrayBuffer(size); + const buffer = new ArrayBuffer(8); const dv = new DataView(buffer); - dv.setUint32(0, n, littleEndian); - return new Uint8Array(buffer); + dv.setBigUint64(0, BigInt(n), littleEndian); + return new Uint8Array(buffer.slice(0, size)); } export function numberToLittleEndianBytes(n: number, s: Size) {