Skip to content

Commit

Permalink
test(connector-fabric-socketio): add functional test, bug fix
Browse files Browse the repository at this point in the history
Added functional jest test fabric-socketio-connector.test that can be run during CI process.
It checks evaluate/sending transactions and monitoring for new events.
Connector had to be refactored to be testable,
tests also discovered some bugs that had to be fixed in order to pass.

SocketIOApiClient refactors:
- Added option for supplying validatorKeyValue instead of validatorKeyPath.
- JWT validation function works with key value now (instead of reading the key).
- Validator can now return messages that are not encrypted (it throwed error previously).
- Adjusted unit tests.

connector-fabric-socketio refactors:
- Connector can be run both as a standalone app and loaded as a module (www.js file).
  Caller can use exported startFabricSocketIOConnector function to run the connector.
  Configuration must be supplied in file or in env variable like it's done in functional test.
- All cryptographic data (keys, certificates, etc…) can now be supplied as a value
  (previously it supported only path to a file).
- sendSignedTransaction can now be called synchronously (it had wrong response format before).
- Fixed a bug introduced during my last changes in this component,
  which caused fabric-client session to be disconnected but still reused by follow-up requests.
  I didn't know that gateway disconnects client it operates on.
- Increased JWT expiration to 15 minutes to prevent constant JWT expiration error
  (I'm pretty sure 15 minutes is still secure period).
- Minor improvements (logging, formatting, etc…)

fabric-test-ledger-v1 changes:
- Added adminCredentials to have programatic access to admin credentials on currently used ledger
 (unlikely, but can change in the future).

Depends on: hyperledger-cacti#1975

Closes: hyperledger-cacti#1976

Signed-off-by: Michal Bajer <[email protected]>
  • Loading branch information
outSH committed May 27, 2022
1 parent e8d1bb1 commit 24fa3fa
Show file tree
Hide file tree
Showing 17 changed files with 1,049 additions and 349 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ site/
!.yarn/versions
.pnp.*

!packages/cactus-plugin-verifier-cc/src/main/typescript/ledger-plugin/*/validator/src/build
!packages/cactus-plugin-verifier-cc/src/main/typescript/ledger-plugin/*/validator/src/core/bin
!packages/cactus-plugin-ledger-connector-*-socketio/src/main/typescript/common/core/bin

tools/docker/geth-testnet/data-geth1/
118 changes: 63 additions & 55 deletions packages/cactus-api-client/src/main/typescript/socketio-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { ISocketApiClient } from "@hyperledger/cactus-core-api";

import { Socket, SocketOptions, ManagerOptions, io } from "socket.io-client";
import { readFile } from "fs";
import { readFileSync } from "fs";
import { resolve as resolvePath } from "path";
import { verify, VerifyOptions, VerifyErrors, JwtPayload } from "jsonwebtoken";
import { Observable, ReplaySubject } from "rxjs";
Expand All @@ -26,40 +26,31 @@ import { finalize } from "rxjs/operators";
/**
* Default logic for validating responses from socketio connector (validator).
* Assumes that message is JWT signed with validator private key.
* @param keyPath - Absolute or relative path to validator public key.
* @param publicKey - Validator public key.
* @param targetData - Signed JWT message to be decoded.
* @returns Promise resolving to decoded JwtPayload.
*/
export function verifyValidatorJwt(
keyPath: string,
publicKey: string,
targetData: string,
): Promise<JwtPayload> {
return new Promise((resolve, reject) => {
readFile(
resolvePath(__dirname, keyPath),
(fileError: Error | null, publicKey: Buffer) => {
if (fileError) {
reject(fileError);
const option: VerifyOptions = {
algorithms: ["ES256"],
};

verify(
targetData,
publicKey,
option,
(err: VerifyErrors | null, decoded: JwtPayload | undefined) => {
if (err) {
reject(err);
} else if (decoded === undefined) {
reject(Error("Decoded message is undefined"));
} else {
resolve(decoded);
}

const option: VerifyOptions = {
algorithms: ["ES256"],
};

verify(
targetData,
publicKey,
option,
(err: VerifyErrors | null, decoded: JwtPayload | undefined) => {
if (err) {
reject(err);
} else if (decoded === undefined) {
reject(Error("Decoded message is undefined"));
} else {
resolve(decoded);
}
},
);
},
);
});
Expand All @@ -71,7 +62,8 @@ export function verifyValidatorJwt(
export type SocketIOApiClientOptions = {
readonly validatorID: string;
readonly validatorURL: string;
readonly validatorKeyPath: string;
readonly validatorKeyValue?: string;
readonly validatorKeyPath?: string;
readonly logLevel?: LogLevelDesc;
readonly maxCounterRequestID?: number;
readonly syncFunctionTimeoutMillisecond?: number;
Expand All @@ -94,20 +86,23 @@ export type SocketLedgerEvent = {
export class SocketIOApiClient implements ISocketApiClient<SocketLedgerEvent> {
private readonly log: Logger;
private readonly socket: Socket;
private readonly validatorKey: string;

// @todo - Why replay only last one? Maybe make it configurable?
private monitorSubject: ReplaySubject<SocketLedgerEvent> | undefined;

readonly className: string;
counterReqID = 1;
checkValidator: (
key: string,
publicKey: string,
data: string,
) => Promise<JwtPayload> = verifyValidatorJwt;

/**
* @param validatorID - (required) ID of validator.
* @param validatorURL - (required) URL to validator socketio endpoint.
* @param validatorKeyPath - (required) Path to validator public key in local storage.
* @param validatorKeyValue - (required if no validatorKeyPath) Validator public key.
* @param validatorKeyPath - (required if no validatorKeyValue) Path to validator public key in local storage.
*/
constructor(public readonly options: SocketIOApiClientOptions) {
this.className = this.constructor.name;
Expand All @@ -120,16 +115,24 @@ export class SocketIOApiClient implements ISocketApiClient<SocketLedgerEvent> {
options.validatorURL,
`${this.className}::constructor() validatorURL`,
);
Checks.nonBlankString(
// TODO - checks path exists?
options.validatorKeyPath,
`${this.className}::constructor() validatorKeyPath`,
);

const level = this.options.logLevel || "INFO";
const label = this.className;
this.log = LoggerProvider.getOrCreate({ level, label });

if (options.validatorKeyValue) {
this.validatorKey = options.validatorKeyValue;
} else if (options.validatorKeyPath) {
this.validatorKey = readFileSync(
resolvePath(__dirname, options.validatorKeyPath),
"ascii",
);
} else {
throw new Error(
"Either validatorKeyValue or validatorKeyPath must be defined",
);
}

this.log.info(
`Created ApiClient for Validator ID: ${options.validatorID}, URL ${options.validatorURL}, KeyPath ${options.validatorKeyPath}`,
);
Expand Down Expand Up @@ -215,25 +218,30 @@ export class SocketIOApiClient implements ISocketApiClient<SocketLedgerEvent> {
if (reqID === result.id) {
responseFlag = true;

this.checkValidator(
this.options.validatorKeyPath,
result.resObj.data,
)
.then((decodedData) => {
this.log.debug("checkValidator decodedData:", decodedData);
const resultObj = {
status: result.resObj.status,
data: decodedData.result,
};
this.log.debug("resultObj =", resultObj);
// Result reply
resolve(resultObj);
})
.catch((err) => {
responseFlag = false;
this.log.debug("checkValidator error:", err);
this.log.error(err);
});
if (typeof result.resObj.data !== "string") {
this.log.debug(
"Response data is probably not encrypted. resultObj =",
result.resObj,
);
resolve(result.resObj);
} else {
this.checkValidator(this.validatorKey, result.resObj.data)
.then((decodedData) => {
this.log.debug("checkValidator decodedData:", decodedData);
const resultObj = {
status: result.resObj.status,
data: decodedData.result,
};
this.log.debug("resultObj =", resultObj);
// Result reply
resolve(resultObj);
})
.catch((err) => {
responseFlag = false;
this.log.debug("checkValidator error:", err);
this.log.error(err);
});
}
}
});

Expand Down Expand Up @@ -327,7 +335,7 @@ export class SocketIOApiClient implements ISocketApiClient<SocketLedgerEvent> {
// output the data received from the client
this.log.debug("#[recv]eventReceived, res:", res);

this.checkValidator(this.options.validatorKeyPath, res.blockData)
this.checkValidator(this.validatorKey, res.blockData)
.then((decodedData) => {
const resultObj = {
status: res.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const log: Logger = LoggerProvider.getOrCreate({
const defaultConfigOptions = {
validatorID: "123validatorId321",
validatorURL: "https://example:1234",
validatorKeyValue: "",
validatorKeyPath: "./nonexistent/path/somekey.crt",
logLevel: sutLogLevel,
maxCounterRequestID: 3,
Expand All @@ -50,19 +51,16 @@ import { generateKeyPairSync } from "crypto";
const { publicKey, privateKey } = generateKeyPairSync("ec", {
namedCurve: "P-256",
});
const publicKeyString = publicKey.export({
type: "spki",
format: "pem",
}) as string;

import { SocketIOTestSetupHelpers } from "@hyperledger/cactus-test-tooling";

// Mock public key reading
import fs from "fs";
jest
.spyOn(fs, "readFile")
.mockImplementation((_: unknown, callback: any) =>
callback(
null,
Buffer.from(publicKey.export({ type: "spki", format: "pem" })),
),
);
jest.spyOn(fs, "readFileSync").mockReturnValue(publicKeyString);

import {
SocketIOApiClient,
Expand All @@ -81,7 +79,6 @@ jest.setTimeout(testTimeout);
//////////////////////////////////

describe("verifyValidatorJwt tests", () => {
const mockKeyPath = "someKeyPath.pem";
const message = {
message: "Hello",
from: "Someone",
Expand All @@ -107,7 +104,7 @@ describe("verifyValidatorJwt tests", () => {
test("Decrypts the payload from the validator using it's public key", async () => {
// Verify (decrypt)
const decryptedMessage = await verifyValidatorJwt(
mockKeyPath,
publicKeyString,
signedMessage,
);

Expand All @@ -117,12 +114,6 @@ describe("verifyValidatorJwt tests", () => {
const decryptedJwt = decryptedMessage as JwtPayload;
expect(decryptedJwt.iat).toBeNumber();
expect(decryptedJwt.exp).toBeNumber();

// Assert reading correct public key
expect(((fs.readFile as unknown) as jest.Mock).mock.calls.length).toBe(1);
expect(((fs.readFile as unknown) as jest.Mock).mock.calls[0][0]).toContain(
mockKeyPath,
);
});

test("Rejects malicious message", () => {
Expand All @@ -131,7 +122,9 @@ describe("verifyValidatorJwt tests", () => {
log.debug("maliciousMessage", maliciousMessage);

// Verify (decrypt)
return expect(verifyValidatorJwt(mockKeyPath, maliciousMessage)).toReject();
return expect(
verifyValidatorJwt(publicKeyString, maliciousMessage),
).toReject();
});

test("Rejects expired message", (done) => {
Expand All @@ -148,9 +141,11 @@ describe("verifyValidatorJwt tests", () => {

setTimeout(async () => {
// Verify after short timeout
await expect(verifyValidatorJwt(mockKeyPath, signedMessage)).toReject();
await expect(
verifyValidatorJwt(publicKeyString, signedMessage),
).toReject();
done();
}, 100);
}, 1000);
});
});

Expand Down Expand Up @@ -184,8 +179,9 @@ describe("Construction Tests", () => {
configOptions.validatorURL = "";
expect(() => new SocketIOApiClient(configOptions)).toThrow();

// Empty validatorKeyPath
// Empty validatorKeyValue and validatorKeyPath
configOptions = cloneDeep(defaultConfigOptions);
configOptions.validatorKeyValue = "";
configOptions.validatorKeyPath = "";
expect(() => new SocketIOApiClient(configOptions)).toThrow();
});
Expand Down Expand Up @@ -367,10 +363,7 @@ describe("SocketIOApiClient Tests", function () {
.resolves.toEqual({ status: responseStatus, data: decryptedData })
.then(() => {
expect(verifyMock).toHaveBeenCalledTimes(1);
expect(verifyMock).toBeCalledWith(
defaultConfigOptions.validatorKeyPath,
encryptedData,
);
expect(verifyMock).toBeCalledWith(publicKeyString, encryptedData);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@hyperledger/cactus-plugin-ledger-connector-fabric-socketio",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "dist/common/core/bin/www.js",
"module": "dist/common/core/bin/www.js",
"types": "dist/common/core/bin/www.d.ts",
"scripts": {
"start": "cd ./dist && node common/core/bin/www.js",
"debug": "nodemon --inspect ./dist/common/core/bin/www.js",
Expand Down Expand Up @@ -30,6 +33,7 @@
"socket.io": "4.4.1"
},
"devDependencies": {
"@hyperledger/cactus-api-client": "1.0.0",
"@types/config": "0.0.41",
"ts-node": "9.1.1"
}
Expand Down
Loading

0 comments on commit 24fa3fa

Please sign in to comment.