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 <justprosh@users.noreply.github.com>
Co-authored-by: Alexey Proshutinskiy <alexey.prosh@fluence.one>
This commit is contained in:
Akim 2023-10-10 23:26:44 +07:00 committed by GitHub
parent c0b73fec4a
commit 230f47d27e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 270 additions and 201 deletions

View File

@ -34,7 +34,7 @@ import { Subject } from 'rxjs';
import { throwIfHasNoPeerId } from '../util/libp2pUtils.js'; import { throwIfHasNoPeerId } from '../util/libp2pUtils.js';
import { IConnection } from './interfaces.js'; import { IConnection } from './interfaces.js';
import { IParticle } from '../particle/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 { identifyService } from 'libp2p/identify';
import { pingService } from 'libp2p/ping'; import { pingService } from 'libp2p/ping';
import { unmarshalPublicKey } from '@libp2p/crypto/keys'; import { unmarshalPublicKey } from '@libp2p/crypto/keys';
@ -186,7 +186,9 @@ export class RelayConnection implements IConnection {
return; 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) { if (isVerified) {
this.particleSource.next(particle); this.particleSource.next(particle);
} else { } else {

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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 import bs58 from "bs58";
const { decode } = bs58.default; 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 keyBytes = toUint8Array(key);
const testData = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9, 10]); 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 // signature produced by KeyPair created from some random KeyPair
describe('KeyPair tests', () => { describe("KeyPair tests", () => {
it('generate keypair from seed', async function () { it("generate keypair from seed", async function () {
// arrange // arrange
const random = await KeyPair.randomEd25519(); const random = await KeyPair.randomEd25519();
const privateKey = random.toEd25519PrivateKey(); const privateKey = random.toEd25519PrivateKey();
// act // act
const keyPair = await KeyPair.fromEd25519SK(privateKey); const keyPair = await KeyPair.fromEd25519SK(privateKey);
const privateKey2 = keyPair.toEd25519PrivateKey(); const privateKey2 = keyPair.toEd25519PrivateKey();
// assert // assert
expect(privateKey).toStrictEqual(privateKey2); expect(privateKey).toStrictEqual(privateKey2);
}); });
it('create keypair from ed25519 private key', async function () { it("create keypair from ed25519 private key", async function () {
// arrange // arrange
const rustSK = 'jDaxLJzYtzgwTMrELJCAqavtmx85ktQNfB2rLcK7MhH'; const rustSK = "jDaxLJzYtzgwTMrELJCAqavtmx85ktQNfB2rLcK7MhH";
const sk = decode(rustSK); const sk = bs58.decode(rustSK);
// act // act
const keyPair = await KeyPair.fromEd25519SK(sk); const keyPair = await KeyPair.fromEd25519SK(sk);
// assert // assert
const expectedPeerId = '12D3KooWH1W3VznVZ87JH4FwABK4mkntcspTVWJDta6c2xg9Pzbp'; const expectedPeerId =
expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId); "12D3KooWH1W3VznVZ87JH4FwABK4mkntcspTVWJDta6c2xg9Pzbp";
});
it('create keypair from a seed phrase', async function () { expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId);
// arrange });
const seedArray = new Uint8Array(32).fill(1);
// act it("create keypair from a seed phrase", async function () {
const keyPair = await KeyPair.fromEd25519SK(seedArray); // arrange
const seedArray = new Uint8Array(32).fill(1);
// assert // act
const expectedPeerId = '12D3KooWK99VoVxNE7XzyBwXEzW7xhK7Gpv85r9F3V3fyKSUKPH5'; const keyPair = await KeyPair.fromEd25519SK(seedArray);
expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId);
});
it('sign', async function () { // assert
// arrange const expectedPeerId =
const keyPair = await KeyPair.fromEd25519SK(keyBytes); "12D3KooWK99VoVxNE7XzyBwXEzW7xhK7Gpv85r9F3V3fyKSUKPH5";
// act expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId);
const res = await keyPair.signBytes(testData); });
// assert
expect(new Uint8Array(res)).toStrictEqual(testDataSig);
});
it('verify', async function () { it("sign", async function () {
// arrange // arrange
const keyPair = await KeyPair.fromEd25519SK(keyBytes); const keyPair = await KeyPair.fromEd25519SK(keyBytes);
// act // act
const res = await keyPair.verify(testData, testDataSig); const res = await keyPair.signBytes(testData);
// assert
expect(new Uint8Array(res)).toStrictEqual(testDataSig);
});
// assert it("verify", async function () {
expect(res).toBe(true); // arrange
}); const keyPair = await KeyPair.fromEd25519SK(keyBytes);
it('sign-verify', async function () { // act
// arrange const res = await keyPair.verify(testData, testDataSig);
const keyPair = await KeyPair.fromEd25519SK(keyBytes);
// act // assert
const data = new Uint8Array(32).fill(1); expect(res).toBe(true);
const sig = await keyPair.signBytes(data); });
const res = await keyPair.verify(data, sig);
// assert it("sign-verify", async function () {
expect(res).toBe(true); // 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==");
});
}); });

View File

@ -1,5 +1,5 @@
/* /**
* Copyright 2020 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,92 +14,105 @@
* limitations under the License. * limitations under the License.
*/ */
import type { PeerId } from '@libp2p/interface/peer-id'; import { KeyPairOptions } from "@fluencelabs/interfaces";
import { generateKeyPairFromSeed, generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys'; import {
import { createFromPrivKey, createFromPubKey } from '@libp2p/peer-id-factory'; generateKeyPairFromSeed,
import type { PrivateKey, PublicKey } from '@libp2p/interface/keys'; generateKeyPair,
import { toUint8Array } from 'js-base64'; unmarshalPublicKey,
import * as bs58 from 'bs58'; } from "@libp2p/crypto/keys";
import { KeyPairOptions } from '@fluencelabs/interfaces'; import type { PrivateKey, PublicKey } from "@libp2p/interface/keys";
import type { PeerId } from "@libp2p/interface/peer-id";
// @ts-ignore import { createFromPrivKey } from "@libp2p/peer-id-factory";
const { decode } = bs58.default; import bs58 from "bs58";
import { toUint8Array } from "js-base64";
export class KeyPair { export class KeyPair {
/** private publicKey: PublicKey;
* Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation
*/
getLibp2pPeerId() {
return this.libp2pPeerId;
}
constructor( private constructor(
private privateKey: PrivateKey | undefined, private privateKey: PrivateKey,
private publicKey: PublicKey, private libp2pPeerId: PeerId,
private libp2pPeerId: PeerId ) {
) {} this.publicKey = privateKey.public;
}
/** /**
* Generates new KeyPair from ed25519 private key represented as a 32 byte array * Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation
* @param seed - Any sequence of 32 bytes */
* @returns - Promise with the created KeyPair getLibp2pPeerId() {
*/ return this.libp2pPeerId;
static async fromEd25519SK(seed: Uint8Array): Promise<KeyPair> { }
const key = await generateKeyPairFromSeed('Ed25519', seed, 256);
const lib2p2Pid = await createFromPrivKey(key);
return new KeyPair(key, key.public, lib2p2Pid);
}
/** /**
* Generates new KeyPair with a random secret key * Return public key inferred from private key
* @returns - Promise with the created KeyPair */
*/ getPublicKey() {
static async randomEd25519(): Promise<KeyPair> { return this.publicKey.bytes;
const key = await generateKeyPair('Ed25519'); }
const lib2p2Pid = await createFromPrivKey(key);
return new KeyPair(key, key.public, lib2p2Pid);
}
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<KeyPair> {
const key = await generateKeyPairFromSeed("Ed25519", seed, 256);
const lib2p2Pid = await createFromPrivKey(key);
return new KeyPair(key, lib2p2Pid);
}
/** /**
* @returns 32 byte private key * Generates new KeyPair with a random secret key
*/ * @returns - Promise with the created KeyPair
toEd25519PrivateKey(): Uint8Array { */
if (this.privateKey === undefined) { static async randomEd25519(): Promise<KeyPair> {
throw new Error('Private key not supplied'); const key = await generateKeyPair("Ed25519");
} const lib2p2Pid = await createFromPrivKey(key);
return this.privateKey.marshal().subarray(0, 32); return new KeyPair(key, lib2p2Pid);
} }
signBytes(data: Uint8Array): Promise<Uint8Array> { static verifyWithPublicKey(
if (this.privateKey === undefined) { publicKey: Uint8Array,
throw new Error('Private key not supplied'); message: Uint8Array,
} signature: Uint8Array,
return this.privateKey.sign(data); ) {
} return unmarshalPublicKey(publicKey).verify(message, signature);
}
verify(data: Uint8Array, signature: Uint8Array): Promise<boolean> { getPeerId(): string {
return this.publicKey.verify(data, signature); return this.libp2pPeerId.toString();
} }
/**
* @returns 32 byte private key
*/
toEd25519PrivateKey(): Uint8Array {
return this.privateKey.marshal().subarray(0, 32);
}
signBytes(data: Uint8Array): Promise<Uint8Array> {
return this.privateKey.sign(data);
}
verify(data: Uint8Array, signature: Uint8Array): Promise<boolean> {
return this.publicKey.verify(data, signature);
}
} }
export const fromBase64Sk = (sk: string): Promise<KeyPair> => { export const fromBase64Sk = (sk: string): Promise<KeyPair> => {
const skArr = toUint8Array(sk); const skArr = toUint8Array(sk);
return KeyPair.fromEd25519SK(skArr); return KeyPair.fromEd25519SK(skArr);
}; };
export const fromBase58Sk = (sk: string): Promise<KeyPair> => { export const fromBase58Sk = (sk: string): Promise<KeyPair> => {
const skArr = decode(sk); const skArr = bs58.decode(sk);
return KeyPair.fromEd25519SK(skArr); return KeyPair.fromEd25519SK(skArr);
}; };
export const fromOpts = (opts: KeyPairOptions): Promise<KeyPair> => { export const fromOpts = (opts: KeyPairOptions): Promise<KeyPair> => {
if (opts.source === 'random') { if (opts.source === "random") {
return KeyPair.randomEd25519(); return KeyPair.randomEd25519();
} }
return KeyPair.fromEd25519SK(opts.source); return KeyPair.fromEd25519SK(opts.source);
}; };

View File

@ -14,15 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { atob, fromUint8Array, toUint8Array } from 'js-base64'; import { CallResultsArray } from "@fluencelabs/avm";
import { CallResultsArray } from '@fluencelabs/avm'; import { fromUint8Array, toUint8Array } from "js-base64";
import { v4 as uuidv4 } from 'uuid'; import { concat } from "uint8arrays/concat";
import { Buffer } from 'buffer'; import { v4 as uuidv4 } from "uuid";
import { IParticle } from './interfaces.js';
import { concat } from 'uint8arrays/concat'; import { KeyPair } from "../keypair/index.js";
import { numberToLittleEndianBytes } from '../util/bytes.js'; import { numberToLittleEndianBytes } from "../util/bytes.js";
import { KeyPair } from '../keypair/index.js';
import { unmarshalPublicKey } from '@libp2p/crypto/keys'; import { IParticle } from "./interfaces.js";
export class Particle implements IParticle { export class Particle implements IParticle {
constructor( constructor(
@ -34,14 +34,31 @@ export class Particle implements IParticle {
public readonly initPeerId: string, public readonly initPeerId: string,
public readonly signature: Uint8Array public readonly signature: Uint8Array
) {} ) {}
static async createNew(script: string, initPeerId: string, ttl: number, keyPair: KeyPair): Promise<Particle> { static async createNew(
const id = uuidv4(); script: string,
const timestamp = Date.now(); initPeerId: string,
const message = buildParticleMessage({ id, timestamp, ttl, script }); ttl: number,
const signature = await keyPair.signBytes(message); keyPair: KeyPair,
return new Particle(id, Date.now(), script, Buffer.from([]), ttl, initPeerId, signature); _id?: string,
} _timestamp?: number,
_data?: Uint8Array,
): Promise<Particle> {
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 { static fromString(str: string): Particle {
const json = JSON.parse(str); const json = JSON.parse(str);
@ -64,27 +81,32 @@ const en = new TextEncoder();
/** /**
* Builds particle message for signing * Builds particle message for signing
*/ */
export const buildParticleMessage = ({ id, timestamp, ttl, script }: Omit<IParticle, 'initPeerId' | 'signature' | 'data'>): Uint8Array => { export const buildParticleMessage = ({
return concat([ id,
en.encode(id), timestamp,
numberToLittleEndianBytes(timestamp, 'u64'), ttl,
numberToLittleEndianBytes(ttl, 'u32'), script,
en.encode(script), }: Omit<IParticle, "initPeerId" | "signature" | "data">): 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 * Returns actual ttl of a particle, i.e. ttl - time passed since particle creation
*/ */
export const getActualTTL = (particle: IParticle): number => { 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 * Returns true if particle has expired
*/ */
export const hasExpired = (particle: IParticle): boolean => { 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 * Creates a particle clone with new data
*/ */
export const cloneWithNewData = (particle: IParticle, newData: Uint8Array): IParticle => { export const cloneWithNewData = (
return new Particle(particle.id, particle.timestamp, particle.script, newData, particle.ttl, particle.initPeerId, particle.signature); particle: IParticle,
}; newData: Uint8Array,
): IParticle => {
/** return new Particle(
* Creates a deep copy of a particle particle.id,
*/ particle.timestamp,
export const fullClone = (particle: IParticle): IParticle => { particle.script,
return JSON.parse(JSON.stringify(particle)); newData,
particle.ttl,
particle.initPeerId,
particle.signature,
);
}; };
/** /**
* Serializes particle into string suitable for sending through network * Serializes particle into string suitable for sending through network
*/ */
export const serializeToString = (particle: IParticle): string => { export const serializeToString = (particle: IParticle): string => {
return JSON.stringify({ return JSON.stringify({
action: 'Particle', action: "Particle",
id: particle.id, id: particle.id,
init_peer_id: particle.initPeerId, init_peer_id: particle.initPeerId,
timestamp: particle.timestamp, timestamp: particle.timestamp,
ttl: particle.ttl, ttl: particle.ttl,
script: particle.script, script: particle.script,
signature: Array.from(particle.signature), signature: Array.from(particle.signature),
data: particle.data && fromUint8Array(particle.data), data: fromUint8Array(particle.data),
}); });
}; };
/** /**
* When particle is executed, it goes through different stages. The type describes all possible stages and their parameters * When particle is executed, it goes through different stages. The type describes all possible stages and their parameters
*/ */
export type ParticleExecutionStage = export type ParticleExecutionStage =
| { stage: 'received' } | { stage: "received" }
| { stage: 'interpreted' } | { stage: "interpreted" }
| { stage: 'interpreterError'; errorMessage: string } | { stage: "interpreterError"; errorMessage: string }
| { stage: 'localWorkDone' } | { stage: "localWorkDone" }
| { stage: 'sent' } | { stage: "sent" }
| { stage: 'sendingError'; errorMessage: string } | { stage: "sendingError"; errorMessage: string }
| { stage: 'expired' }; | { stage: "expired" };
/** /**
* Particle queue item is a wrapper around particle, which contains additional information about particle execution * Particle queue item is a wrapper around particle, which contains additional information about particle execution
*/ */
export interface ParticleQueueItem { export interface ParticleQueueItem {
particle: IParticle; particle: IParticle;
callResults: CallResultsArray; callResults: CallResultsArray;
onStageChange: (state: ParticleExecutionStage) => void; onStageChange: (state: ParticleExecutionStage) => void;
} }
/** /**
* Helper function to handle particle at expired stage * Helper function to handle particle at expired stage
*/ */
export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => { export const handleTimeout = (fn: () => void) => {
if (stage.stage === 'expired') { return (stage: ParticleExecutionStage) => {
fn(); if (stage.stage === "expired") {
} fn();
}; }
}
}

View File

@ -23,10 +23,10 @@ const sizeMap = {
function numberToBytes(n: number, s: Size, littleEndian: boolean) { function numberToBytes(n: number, s: Size, littleEndian: boolean) {
const size = sizeMap[s]; const size = sizeMap[s];
const buffer = new ArrayBuffer(size); const buffer = new ArrayBuffer(8);
const dv = new DataView(buffer); const dv = new DataView(buffer);
dv.setUint32(0, n, littleEndian); dv.setBigUint64(0, BigInt(n), littleEndian);
return new Uint8Array(buffer); return new Uint8Array(buffer.slice(0, size));
} }
export function numberToLittleEndianBytes(n: number, s: Size) { export function numberToLittleEndianBytes(n: number, s: Size) {