diff --git a/package.json b/package.json index 6850647c..084aaa79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluence", - "version": "0.4.9", + "version": "0.5.0", "description": "the browser js-libp2p client for the Fluence network", "main": "./dist/fluence.js", "typings": "./dist/fluence.d.ts", diff --git a/src/fluence_client.ts b/src/fluence_client.ts index c50326c7..45b6eded 100644 --- a/src/fluence_client.ts +++ b/src/fluence_client.ts @@ -24,7 +24,7 @@ import * as PeerInfo from "peer-info"; import {FluenceConnection} from "./fluence_connection"; export class FluenceClient { - private readonly selfPeerInfo: PeerInfo; + readonly selfPeerInfo: PeerInfo; readonly selfPeerIdStr: string; private connection: FluenceConnection; @@ -51,7 +51,7 @@ export class FluenceClient { * @param predicate will be applied to each incoming call until it matches */ waitResponse(predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined)): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, _) => { // subscribe for responses, to handle response // TODO if there's no conn, reject this.subscribe((args: any, target: Address, replyTo: Address) => { @@ -162,7 +162,10 @@ export class FluenceClient { } } catch (e) { // if service throw an error, return it to the sender - return FluenceClient.responseCall(call.reply_to, {reason: `error on execution: ${e}`, msg: call}); + return FluenceClient.responseCall(call.reply_to, { + reason: `error on execution: ${e}`, + msg: call + }); } return undefined; @@ -171,7 +174,10 @@ export class FluenceClient { console.log(`relay call: ${call}`); } else { console.warn(`this relay call is not for me: ${callToString(call)}`); - return FluenceClient.responseCall(call.reply_to, {reason: `this relay call is not for me`, msg: call}); + return FluenceClient.responseCall(call.reply_to, { + reason: `this relay call is not for me`, + msg: call + }); } return undefined; case ProtocolType.Peer: @@ -179,7 +185,10 @@ export class FluenceClient { console.log(`peer call: ${call}`); } else { console.warn(`this peer call is not for me: ${callToString(call)}`); - return FluenceClient.responseCall(call.reply_to, {reason: `this relay call is not for me`, msg: call}); + return FluenceClient.responseCall(call.reply_to, { + reason: `this relay call is not for me`, + msg: call + }); } return undefined; } @@ -203,7 +212,6 @@ export class FluenceClient { } - /** * Sends a call to unregister the service_id. */ diff --git a/src/fluence_connection.ts b/src/fluence_connection.ts index 94aa6e0b..ea84e68d 100644 --- a/src/fluence_connection.ts +++ b/src/fluence_connection.ts @@ -162,7 +162,7 @@ export class FluenceConnection { private async sendCall(call: FunctionCall) { let callStr = callToString(call); - console.log("send function call: " + callStr); + console.log("send function call: " + JSON.stringify(JSON.parse(callStr), undefined, 2)); console.log(call); // create outgoing substream diff --git a/src/test/address.spec.ts b/src/test/address.spec.ts index 2f34c4cf..98b7600c 100644 --- a/src/test/address.spec.ts +++ b/src/test/address.spec.ts @@ -11,6 +11,9 @@ import 'mocha'; import * as PeerId from "peer-id"; import {callToString, genUUID, makeFunctionCall, parseFunctionCall} from "../function_call"; import Fluence from "../fluence"; +import {certificateFromString, certificateToString, issue} from "../trust/certificate"; +import {TrustGraph} from "../trust/trust_graph"; +import {nodeRootCert} from "../trust/misc"; describe("Typescript usage suite", () => { @@ -89,14 +92,67 @@ describe("Typescript usage suite", () => { }); + it("should serialize and deserialize certificate correctly", async function () { + let cert = `11 +1111 +5566Dn4ZXXbBK5LJdUsE7L3pG9qdAzdPY47adjzkhEx9 +3HNXpW2cLdqXzf4jz5EhsGEBFkWzuVdBCyxzJUZu2WPVU7kpzPjatcqvdJMjTtcycVAdaV5qh2fCGphSmw8UMBkr +158981172690500 +1589974723504 +2EvoZAZaGjKWFVdr36F1jphQ5cW7eK3yM16mqEHwQyr7 +4UAJQWzB3nTchBtwARHAhsn7wjdYtqUHojps9xV6JkuLENV8KRiWM3BhQByx5KijumkaNjr7MhHjouLawmiN1A4d +1590061123504 +1589974723504` + + let deser = await certificateFromString(cert); + let ser = certificateToString(deser); + + expect(ser).to.be.equal(cert); + }); + + // delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes it("integration test", async function () { - this.timeout(5000); - await testCalculator(); + this.timeout(15000); + await testCerts(); + // await testCalculator(); }); }); const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); +export async function testCerts() { + let key1 = await Fluence.generatePeerId(); + let key2 = await Fluence.generatePeerId(); + + // connect to two different nodes + let cl1 = await Fluence.connect("/dns4/104.248.25.59/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1); + let cl2 = await Fluence.connect("/ip4/104.248.25.59/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2); + + let trustGraph1 = new TrustGraph(cl1); + let trustGraph2 = new TrustGraph(cl2); + + let issuedAt = new Date(); + let expiresAt = new Date(); + // certificate expires after one day + expiresAt.setDate(new Date().getDate() + 1); + + // create root certificate for key1 and extend it with key2 + let rootCert = await nodeRootCert(key1); + let extended = await issue(key1, key2, rootCert, expiresAt.getTime(), issuedAt.getTime()); + + // publish certificates to Fluence network + await trustGraph1.publishCertificates(key2.toB58String(), [extended]); + + // get certificates from network + let certs = await trustGraph2.getCertificates(key2.toB58String()); + + // root certificate could be different because nodes save trusts with bigger `expiresAt` date and less `issuedAt` date + expect(certs[0].chain[1].issuedFor.toB58String()).to.be.equal(extended.chain[1].issuedFor.toB58String()) + expect(certs[0].chain[1].signature).to.be.equal(extended.chain[1].signature) + expect(certs[0].chain[1].expiresAt).to.be.equal(extended.chain[1].expiresAt) + expect(certs[0].chain[1].issuedAt).to.be.equal(extended.chain[1].issuedAt) +} + // Shows how to register and call new service in Fluence network export async function testCalculator() { diff --git a/src/trust/certificate.ts b/src/trust/certificate.ts new file mode 100644 index 00000000..1acf6083 --- /dev/null +++ b/src/trust/certificate.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2020 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createTrust, Trust, trustFromString, trustToString} from "./trust"; +import * as PeerId from "peer-id"; + +const FORMAT = "11"; +const VERSION = "1111"; + +// TODO verify certificate +// Chain of trusts started from self-signed root trust. +export interface Certificate { + chain: Trust[] +} + +export function certificateToString(cert: Certificate): string { + let certStr = cert.chain.map(t => trustToString(t)).join("\n"); + return `${FORMAT}\n${VERSION}\n${certStr}` +} + +export async function certificateFromString(str: string): Promise { + let lines = str.split("\n"); + // last line could be empty + if (!lines[lines.length - 1]) { + lines.pop() + } + + // TODO do match different formats and versions + let _format = lines[0]; + let _version = lines[1]; + console.log("LENGTH: " + lines.length) + + // every trust is 4 lines, certificate lines number without format and version should be divided by 4 + if ((lines.length - 2) % 4 !== 0) { + throw Error("Incorrect format of the certificate:\n" + str); + } + + let chain: Trust[] = []; + + let i; + for(i = 2; i < lines.length; i = i + 4) { + chain.push(await trustFromString(lines[i], lines[i+1], lines[i+2], lines[i+3])) + } + + return {chain}; +} + +// Creates new certificate with root trust (self-signed public key) from a key pair. +export async function issueRoot(issuedBy: PeerId, + forPk: PeerId, + expiresAt: number, + issuedAt: number, +): Promise { + if (expiresAt < issuedAt) { + throw Error("Expiration time should be greater then issued time.") + } + + let maxDate = new Date(158981172690500).getTime(); + + let rootTrust = await createTrust(issuedBy, issuedBy, maxDate, issuedAt); + let trust = await createTrust(forPk, issuedBy, expiresAt, issuedAt); + let chain = [rootTrust, trust]; + + return { + chain: chain + } +} + +// Adds a new trust into chain of trust in certificate. +export async function issue(issuedBy: PeerId, + forPk: PeerId, + extendCert: Certificate, + expiresAt: number, + issuedAt: number): Promise { + if (expiresAt < issuedAt) { + throw Error("Expiration time should be greater then issued time.") + } + + let lastTrust = extendCert.chain[extendCert.chain.length - 1]; + + if (lastTrust.issuedFor !== issuedBy) { + throw Error("`issuedFor` should be equal to `issuedBy` in the last trust of the chain.") + } + + let trust = await createTrust(forPk, issuedBy, expiresAt, issuedAt); + let chain = [...extendCert.chain]; + chain.push(trust); + + return { + chain: chain + } +} diff --git a/src/trust/misc.ts b/src/trust/misc.ts new file mode 100644 index 00000000..71b2f320 --- /dev/null +++ b/src/trust/misc.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as PeerId from "peer-id"; +import {keys} from "libp2p-crypto"; +import {Certificate, issueRoot} from "./certificate"; + +/** + * Generate root certificate with one of the Fluence trusted key for one day. + */ +export async function nodeRootCert(issuedFor: PeerId): Promise { + let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201]; + + let privateK = await keys.generateKeyPairFromSeed("Ed25519", Uint8Array.from(seed), 256); + let peerId = await PeerId.createFromPrivKey(privateK.bytes); + + let issuedAt = new Date(); + let expiresAt = new Date(); + expiresAt.setDate(new Date().getDate() + 1); + + return await issueRoot(peerId, issuedFor, expiresAt.getTime(), issuedAt.getTime()); +} diff --git a/src/trust/trust.ts b/src/trust/trust.ts new file mode 100644 index 00000000..7de0d1c1 --- /dev/null +++ b/src/trust/trust.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as PeerId from "peer-id"; +import {decode, encode} from "bs58" +import crypto from 'libp2p-crypto'; +const ed25519 = crypto.keys.supportedKeys.ed25519; + +// One element in chain of trust in a certificate. +export interface Trust { + issuedFor: PeerId, + expiresAt: number, + signature: string, + issuedAt: number +} + +export function trustToString(trust: Trust): string { + return `${encode(trust.issuedFor.pubKey.marshal())}\n${trust.signature}\n${trust.expiresAt}\n${trust.issuedAt}` +} + +export async function trustFromString(issuedFor: string, signature: string, expiresAt: string, issuedAt: string): Promise { + let pubKey = ed25519.unmarshalEd25519PublicKey(decode(issuedFor)); + let peerId = await PeerId.createFromPubKey(pubKey.bytes); + + return { + issuedFor: peerId, + signature: signature, + expiresAt: parseInt(expiresAt), + issuedAt: parseInt(issuedAt) + } +} + +export async function createTrust(forPk: PeerId, issuedBy: PeerId, expiresAt: number, issuedAt: number): Promise { + let bytes = toSignMessage(forPk, expiresAt, issuedAt); + let signature = await issuedBy.privKey.sign(Buffer.from(bytes)); + let signatureStr = encode(signature); + + return { + issuedFor: forPk, + expiresAt: expiresAt, + signature: signatureStr, + issuedAt: issuedAt + }; +} + +function toSignMessage(pk: PeerId, expiresAt: number, issuedAt: number): Uint8Array { + let bytes = new Uint8Array(48); + let pkEncoded = pk.pubKey.marshal(); + + bytes.set(pkEncoded, 0); + bytes.set(numToArray(expiresAt), 32); + bytes.set(numToArray(issuedAt), 40); + + return bytes +} + +function numToArray(n: number): number[] { + let byteArray = [0, 0, 0, 0, 0, 0, 0, 0]; + + for (let index = 0; index < byteArray.length; index++) { + let byte = n & 0xff; + byteArray [index] = byte; + n = (n - byte) / 256; + } + + return byteArray; +} diff --git a/src/trust/trust_graph.ts b/src/trust/trust_graph.ts new file mode 100644 index 00000000..12a0355c --- /dev/null +++ b/src/trust/trust_graph.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2020 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {FluenceClient} from "../fluence_client"; +import {Certificate, certificateFromString, certificateToString} from "./certificate"; +import {genUUID} from "../function_call"; + +// The client to interact with the Fluence trust graph API +export class TrustGraph { + + client: FluenceClient; + + constructor(client: FluenceClient) { + this.client = client; + } + + // Publish certificate to Fluence network. It will be published in Kademlia neighbourhood by `peerId` key. + async publishCertificates(peerId: string, certs: Certificate[]) { + let certsStr = []; + for (let cert of certs) { + certsStr.push(await certificateToString(cert)); + } + + let msgId = genUUID() + + let response = await this.client.sendServiceCallWaitResponse("add_certificates", { + certificates: certsStr, + msg_id: msgId, + peer_id: peerId + }, (args) => { + // check if it is a successful response + let isSuccessResponse = args.msg_id && args.msg_id === msgId + if (isSuccessResponse) { + return true + } else { + // check if it is an error for this msgId + return args.call && args.call.arguments && args.call.arguments.msg_id === msgId + } + + }); + + if (response.reason) { + throw Error(response.reason) + } else if (response.status) { + return response.status + } else { + throw Error(`Unexpected response: ${response}. Should be 'status' field for a success response or 'reason' field for an error.`) + } + } + + // Get certificates that stores in Kademlia neighbourhood by `peerId` key. + async getCertificates(peerId: string): Promise { + let msgId = genUUID(); + let resp = await this.client.sendServiceCallWaitResponse("certificates", { + msg_id: msgId, + peer_id: peerId + }, (args) => args.msg_id && args.msg_id === msgId) + + let certificatesRaw = resp.certificates + + if (!(certificatesRaw && Array.isArray(certificatesRaw))) { + console.log(Array.isArray(certificatesRaw)) + throw Error("Unexpected. Certificates should be presented in the response as an array.") + } + + let certs = []; + for (let cert of certificatesRaw) { + certs.push(await certificateFromString(cert)) + } + + return certs; + } +}