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 { 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 {

View File

@ -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==");
});
});

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");
* 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<KeyPair> {
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<KeyPair> {
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<KeyPair> {
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<KeyPair> {
const key = await generateKeyPair("Ed25519");
const lib2p2Pid = await createFromPrivKey(key);
return new KeyPair(key, lib2p2Pid);
}
signBytes(data: Uint8Array): Promise<Uint8Array> {
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<boolean> {
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<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> => {
const skArr = toUint8Array(sk);
return KeyPair.fromEd25519SK(skArr);
const skArr = toUint8Array(sk);
return KeyPair.fromEd25519SK(skArr);
};
export const fromBase58Sk = (sk: string): Promise<KeyPair> => {
const skArr = decode(sk);
return KeyPair.fromEd25519SK(skArr);
const skArr = bs58.decode(sk);
return KeyPair.fromEd25519SK(skArr);
};
export const fromOpts = (opts: KeyPairOptions): Promise<KeyPair> => {
if (opts.source === 'random') {
return KeyPair.randomEd25519();
}
if (opts.source === "random") {
return KeyPair.randomEd25519();
}
return KeyPair.fromEd25519SK(opts.source);
return KeyPair.fromEd25519SK(opts.source);
};

View File

@ -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<Particle> {
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<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 {
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<IParticle, 'initPeerId' | 'signature' | 'data'>): Uint8Array => {
return concat([
en.encode(id),
numberToLittleEndianBytes(timestamp, 'u64'),
numberToLittleEndianBytes(ttl, 'u32'),
en.encode(script),
]);
}
export const buildParticleMessage = ({
id,
timestamp,
ttl,
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
*/
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();
}
}
}

View File

@ -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) {