Skip to content

Commit

Permalink
feat(plugin-consortium-manual): JSON Web Signatures for Nodes, Consor…
Browse files Browse the repository at this point in the history
…tium

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed Aug 1, 2020
1 parent 4d897de commit 62a051c
Show file tree
Hide file tree
Showing 67 changed files with 2,324 additions and 932 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Expand Up @@ -288,7 +288,7 @@ below applies to all tests regardless of their nature.
assert.end(); // yaay, test coverage
});
```
- An [end to end test case](./packages/cactus-test-plugin-web-service-consortium/src/test/typescript/integration/plugin-web-service-consortium/security-isolation-via-api-server-ports.ts) showcasing everything in action
- An [end to end test case](./packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/security-isolation-via-api-server-ports.ts) showcasing everything in action
that is being preached in this document about test automation
- Focus/verify a single bug-fix/feature/etc.
- Clearly separated from non-test (aka `main`) source code.
Expand Down Expand Up @@ -353,7 +353,7 @@ for both them separately anyway:
- An integration test:

```sh
npx tap --timeout=600 packages/cactus-test-plugin-web-service-consortium/src/test/typescript/integration/plugin-web-service-consortium/security-isolation-via-api-server-ports.ts
npx tap --timeout=600 packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/security-isolation-via-api-server-ports.ts
```

- A unit test:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -6,6 +6,7 @@
"configure": "lerna clean --yes && lerna bootstrap && npm-run-all build generate-api-server-config",
"generate-api-server-config": "node ./tools/generate-api-server-config.js",
"start:api-server": "node ./packages/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js --config-file=.config.json",
"start:cockpit": "lerna run --scope '*/cactus-cockpit' --stream serve:proxy",
"export-open-api-spec": "ts-node -e 'import(\"./packages/cactus-cmd-api-server/src/main/typescript/openapi-spec\").then((x) => x.exportToFileSystemAsJson());'",
"pregenerate-sdk": "npm-run-all export-open-api-spec",
"generate-sdk": "openapi-generator generate --input-spec cactus-openapi-spec.json -g typescript-axios -o packages/cactus-sdk/src/main/typescript/generated/openapi/typescript-axios/",
Expand All @@ -24,7 +25,7 @@
"build:dev:core-api": "lerna exec --stream --scope '*/*core-api' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"build:dev:test-tooling": "lerna exec --stream --scope '*/*test-tooling' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"build:dev:plugin-ledger-connector-quorum": "lerna exec --stream --scope '*/*connector-quorum' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"build:dev:plugin-web-service-consortium": "lerna exec --stream --scope '*/*web-service-consortium' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"build:dev:plugin-consortium-manual": "lerna exec --stream --scope '*/*manual-consortium' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"build:dev:sdk": "lerna exec --stream --scope '*/*sdk' -- 'del-cli dist/** && tsc --project ./tsconfig.json && webpack --env=dev --target=node --config ../../webpack.config.js'",
"webpack": "npm-run-all webpack:dev webpack:prod",
"webpack:dev": "lerna run webpack:dev",
Expand Down Expand Up @@ -88,4 +89,4 @@
"lint-staged": {
"./**/*.{ts,js}": "prettier --write --config .prettierrc.json"
}
}
}
13 changes: 13 additions & 0 deletions packages/cactus-cmd-api-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/cactus-cmd-api-server/package.json
Expand Up @@ -67,7 +67,7 @@
"@hyperledger/cactus-core-api": "0.2.0",
"@hyperledger/cactus-plugin-keychain-memory": "0.2.0",
"@hyperledger/cactus-plugin-kv-storage-memory": "0.2.0",
"@hyperledger/cactus-plugin-web-service-consortium": "0.2.0",
"@hyperledger/cactus-plugin-consortium-manual": "0.2.0",
"body-parser": "1.19.0",
"compression": "1.7.4",
"convict": "6.0.0",
Expand All @@ -77,6 +77,7 @@
"express-http-proxy": "1.6.0",
"express-openapi-validator": "3.10.0",
"joi": "14.3.1",
"jose": "1.27.2",
"js-sha3": "0.8.0",
"node-fetch": "3.0.0-beta.4",
"node-forge": "0.9.1",
Expand All @@ -99,4 +100,4 @@
"@types/semver": "7.3.1",
"@types/uuid": "7.0.2"
}
}
}
Expand Up @@ -16,18 +16,15 @@ import express, {
import { OpenApiValidator } from "express-openapi-validator";
import compression from "compression";
import bodyParser from "body-parser";
import cors, { CorsOptions } from "cors";
import cors from "cors";
import {
PluginFactory,
ICactusPlugin,
isIPluginWebService,
IPluginWebService,
PluginRegistry,
} from "@hyperledger/cactus-core-api";
import {
ICactusApiServerOptions as ICactusApiServerConfig,
ConfigService,
} from "./config/config-service";
import { ICactusApiServerOptions } from "./config/config-service";
import { CACTUS_OPEN_API_JSON } from "./openapi-spec";
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
import { Servers } from "./common/servers";
Expand All @@ -36,7 +33,7 @@ export interface IApiServerConstructorOptions {
pluginRegistry?: PluginRegistry;
httpServerApi?: Server | SecureServer;
httpServerCockpit?: Server | SecureServer;
config: ICactusApiServerConfig;
config: ICactusApiServerOptions;
}

export class ApiServer {
Expand Down
@@ -1,17 +1,17 @@
import { randomBytes } from "crypto";
import { SecureVersion } from "tls";
import { existsSync, readFileSync } from "fs";
import convict, { Schema, Config, SchemaObj } from "convict";
import { ipaddress } from "convict-format-with-validator";
import secp256k1 from "secp256k1";
import { v4 as uuidV4 } from "uuid";
import { JWK, JWS } from "jose";
import {
LoggerProvider,
Logger,
LogLevelDesc,
} from "@hyperledger/cactus-common";
import { FORMAT_PLUGIN_ARRAY } from "./convict-plugin-array-format";
import { SelfSignedPkiGenerator, IPki } from "./self-signed-pki-generator";
import { Consortium } from "@hyperledger/cactus-plugin-consortium-manual";

convict.addFormat(FORMAT_PLUGIN_ARRAY);
convict.addFormat(ipaddress);
Expand All @@ -24,6 +24,7 @@ export interface IPluginImport {
export interface ICactusApiServerOptions {
configFile: string;
cactusNodeId: string;
consortiumId: string;
logLevel: LogLevelDesc;
tlsDefaultMaxVersion: SecureVersion;
cockpitHost: string;
Expand All @@ -45,10 +46,8 @@ export interface ICactusApiServerOptions {
apiTlsKeyPem: string;
apiTlsClientCaPem: string;
plugins: IPluginImport[];
publicKey: string;
privateKey: string;
keychainSuffixPublicKey: string;
keychainSuffixPrivateKey: string;
keyPairPem: string;
keychainSuffixKeyPairPem: string;
minNodeVersion: string;
}

Expand Down Expand Up @@ -106,6 +105,15 @@ export class ConfigService {
env: "CONFIG_FILE",
arg: "config-file",
},
consortiumId: {
doc:
"Identifier of the consortium your node is part of. " +
" Can be any string of characters such as a UUID",
format: ConfigService.formatNonBlankString,
default: null as any,
env: "CONSORTIUM_ID",
arg: "consortium-id",
},
cactusNodeId: {
doc:
"Identifier of this particular Cactus node. Must be unique among the total set of Cactus nodes running in any " +
Expand Down Expand Up @@ -311,38 +319,26 @@ export class ConfigService {
arg: "api-tls-key-pem",
default: null as any,
},
publicKey: {
doc: "Public key of this Cactus node (the API server)",
env: "PUBLIC_KEY",
arg: "public-key",
format: ConfigService.formatNonBlankString,
default: null as any,
},
privateKey: {
keyPairPem: {
sensitive: true,
doc: "Private key of this Cactus node (the API server)",
env: "PRIVATE_KEY",
arg: "private-key",
doc:
"Key pair (private+public) of this Cactus node in the standard " +
" PEM format.",
env: "KEY_PAIR_PEM",
arg: "key-pair-pem",
format: ConfigService.formatNonBlankString,
default: null as any,
},
keychainSuffixPrivateKey: {
keychainSuffixKeyPairPem: {
doc:
"The key under which to store/retrieve the private key from the keychain of this Cactus node (API server)" +
"The complete lookup key is constructed from the ${CACTUS_NODE_ID}${KEYCHAIN_SUFFIX_PRIVATE_KEY} template.",
env: "KEYCHAIN_SUFFIX_PRIVATE_KEY",
arg: "keychain-suffix-private-key",
"The key under which to store/retrieve the key pair PEM from the " +
" keychain of this Cactus node (API server) The complete lookup key" +
" is constructed from the ${CACTUS_NODE_ID}" +
"${KEYCHAIN_SUFFIX_KEY_PAIR_PEM} template.",
env: "KEYCHAIN_SUFFIX_KEY_PAIR_PEM",
arg: "keychain-suffix-key-pair-pem",
format: "*",
default: "CACTUS_NODE_PRIVATE_KEY",
},
keychainSuffixPublicKey: {
doc:
"The key under which to store/retrieve the public key from the keychain of this Cactus node (API server)" +
"The complete lookup key is constructed from the ${CACTUS_NODE_ID}${KEYCHAIN_SUFFIX_PRIVATE_KEY} template.",
env: "KEYCHAIN_SUFFIX_PUBLIC_KEY",
arg: "keychain-suffix-public-key",
format: "*",
default: "CACTUS_NODE_PUBLIC_KEY",
default: "CACTUS_NODE_KEY_PAIR_PEM",
},
};
}
Expand Down Expand Up @@ -406,22 +402,31 @@ export class ConfigService {
public newExampleConfig(): ICactusApiServerOptions {
const schema = ConfigService.getConfigSchema();

// FIXME most of this lowever level crypto code should be in a commons package that's universal
let privateKeyBytes;
do {
privateKeyBytes = randomBytes(32);
} while (!secp256k1.privateKeyVerify(privateKeyBytes));

const publicKeyBytes = secp256k1.publicKeyCreate(privateKeyBytes);
const privateKey = Buffer.from(privateKeyBytes).toString("hex");
const publicKey = Buffer.from(publicKeyBytes).toString("hex");

const apiTlsEnabled: boolean = (schema.apiTlsEnabled as SchemaObj).default;
const apiHost = (schema.apiHost as SchemaObj).default;
const apiPort = (schema.apiPort as SchemaObj).default;
const apiProtocol = apiTlsEnabled ? "https:" : "http";
const apiBaseUrl = `${apiProtocol}//${apiHost}:${apiPort}`;

// const apiProtocol = apiTlsEnabled ? "https:" : "http";
// const apiBaseUrl = `${apiProtocol}//${apiHost}:${apiPort}`;
const keyPair = JWK.generateSync("EC", "secp256k1", { use: "sig" }, true);
const keyPairPem = keyPair.toPEM(true);
const consortium: Consortium = {
name: "Example Cactus Consortium",
id: uuidV4(),
mainApiHost: apiBaseUrl,
members: [
{
id: uuidV4(),
name: "Example Cactus Consortium Member 1",
nodes: [
{
nodeApiHost: apiBaseUrl,
publicKeyPem: keyPair.toPEM(false),
},
],
},
],
};

const cockpitTlsEnabled: boolean = (schema.cockpitTlsEnabled as SchemaObj)
.default;
Expand All @@ -445,16 +450,18 @@ export class ConfigService {
options: {},
},
{
packageName: "@hyperledger/cactus-plugin-web-service-consortium",
packageName: "@hyperledger/cactus-plugin-consortium-manual",
options: {
privateKey,
keyPairPem,
consortium,
},
},
];

return {
configFile: ".config.json",
cactusNodeId: uuidV4(),
consortiumId: uuidV4(),
logLevel: "debug",
minNodeVersion: (schema.minNodeVersion as SchemaObj).default,
tlsDefaultMaxVersion: "TLSv1.3",
Expand All @@ -476,11 +483,8 @@ export class ConfigService {
cockpitTlsCertPem: pkiServer.certificatePem,
cockpitTlsKeyPem: pkiServer.privateKeyPem,
cockpitTlsClientCaPem: "-", // Cockpit mTLS is off so this will not crash the server
publicKey,
privateKey,
keychainSuffixPublicKey: (schema.keychainSuffixPublicKey as SchemaObj)
.default,
keychainSuffixPrivateKey: (schema.keychainSuffixPrivateKey as SchemaObj)
keyPairPem,
keychainSuffixKeyPairPem: (schema.keychainSuffixKeyPairPem as SchemaObj)
.default,
plugins,
};
Expand Down Expand Up @@ -510,24 +514,22 @@ export class ConfigService {
}

/**
* Validation that prevents operators from mistakenly deploying a public key
* that they may not have the private key for or vica versa.
* Validation that prevents operators from mistakenly deploying a key pair
* that they may not be operational for whatever reason.
*
* @throws If the private key and the public key are not part of the same key pair.
* @throws If a dummy sign+verification operation fails for any reason.
*/
validateKeyPairMatch(): void {
const fnTag = "ConfigService#validateKeyPairMatch()";
// FIXME most of this lowever level crypto code should be in a commons package that's universal
const privateKey = ConfigService.config.get("privateKey");
const privateKeyBytes = Uint8Array.from(Buffer.from(privateKey, "hex"));
const publicKey = ConfigService.config.get("publicKey");
const expectedPublicKeyBytes = secp256k1.publicKeyCreate(privateKeyBytes);
const expectedPublicKey = Buffer.from(expectedPublicKeyBytes).toString(
"hex"
);
if (publicKey !== expectedPublicKey) {
throw new Error(
`Public key does not match private key. Configured=${publicKey} Expected=${expectedPublicKey}`
);
const keyPairPem = ConfigService.config.get("keyPairPem");
const keyPair = JWK.asKey(keyPairPem);

const jws = JWS.sign({ hello: "world" }, keyPair);
try {
JWS.verify(jws, keyPair);
} catch (ex) {
throw new Error(`${fnTag} Invalid key pair PEM: ${ex && ex.stack}`);
}
}
}
11 changes: 11 additions & 0 deletions packages/cactus-cockpit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 62a051c

Please sign in to comment.