diff --git a/.cspell.json b/.cspell.json index 20ac6f181e..2e5dabf6f8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -14,6 +14,7 @@ "authzn", "Besu", "Bools", + "brioux", "cafile", "caio", "cccs", diff --git a/packages/cactus-plugin-ledger-connector-fabric/README.md b/packages/cactus-plugin-ledger-connector-fabric/README.md index fe8e099c8f..0be54a011c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/README.md +++ b/packages/cactus-plugin-ledger-connector-fabric/README.md @@ -46,7 +46,7 @@ The above diagram shows the sequence diagraom of transact() method of the Plugin ![run-transaction-endpoint-enroll](docs/architecture/images/run-transaction-endpoint-enroll.png) -The above diagram shows the sequence diagraom of enroll() method of the PluginLedgerConnectorFabric class. The caller to this function, which in reference to the above sequence diagram is API server, sends Signer object along with EnrollmentRequest as an argument to the enroll() method. Based on the singerType (FabricSigningCredentialType.X509, FabricSigningCredentialType.VaultX509 .. more in TODO), corresponding identity is enrolled and stored inside keychain. +The above diagram shows the sequence diagraom of enroll() method of the PluginLedgerConnectorFabric class. The caller to this function, which in reference to the above sequence diagram is API server, sends Signer object along with EnrollmentRequest as an argument to the enroll() method. Based on the singerType (FabricSigningCredentialType.X509, FabricSigningCredentialType.VaultX509, FabricSigningCredentialType.WsX509), corresponding identity is enrolled and stored inside keychain. ## Usage @@ -78,30 +78,40 @@ const vaultConfig:IVaultConfig = { endpoint : "http://localhost:8200", transitEngineMountPath: "/transit", } +// web-socket server config for supporting vault identity provider +const webSocketConfig:IVaultConfig = { + server: socketServer as FabricSocketServer +} // provide list of identity signing to be supported -const supportedIdentity:FabricSigningCredentialType[] = [FabricSigningCredentialType.VaultX509,FabricSigningCredentialType.X509] +const supportedIdentity:FabricSigningCredentialType[] = [FabricSigningCredentialType.VaultX509,FabricSigningCredentialType.WsX509,FabricSigningCredentialType.X509] const pluginOptions:IPluginLedgerConnectorFabricOptions = { // other options vaultConfig : vaultConfig, + webSocketConfig : webSocketConfig, supportedIdentity:supportedIdentity // .. other options } const connector: PluginLedgerConnectorFabric = new PluginLedgerConnectorFabric(pluginOptions); ``` - To enroll an identity ```typescript await connector.enroll( { keychainId: "keychain-identifier-for storing-certData", keychainRef: "cert-data-identifier", - type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 // require in case of vault + type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 vaultTransitKey: { token: "vault-token", keyName: "vault-key-label", }, + // required in case of web-socket server + type: FabricSigningCredentialType.WsX509, + webSocketKey: { + signature: signature, + sessionId: sessionId, + }, }, { enrollmentID: "client2", @@ -111,19 +121,26 @@ await connector.enroll( }, ); ``` + To Register an identity using register's key ```typescript const secret = await connector.register( { keychainId: "keychain-id-that-store-certData-of-registrar", keychainRef: "certData-label", - type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 // require in case of vault + type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 vaultTransitKey: { token: testToken, keyName: registrarKey, }, + // required in case of web-socket server + type: FabricSigningCredentialType.WsX509, + webSocketKey: { + signature: signature, + sessionId: sessionId, + }, }, { enrollmentID: "client-enrollmentID", @@ -140,13 +157,19 @@ const resp = await connector.transact{ signingCredential: { keychainId: keychainId, keychainRef: "client-certData-id", - type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 // require in case of vault + type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 vaultTransitKey: { token: testToken, keyName: registrarKey, }, + // required in case of web-socket server + type: FabricSigningCredentialType.WsX509, + webSocketKey: { + signature: signature, + sessionId: sessionId, + }, }, // .. other options } @@ -165,6 +188,9 @@ await connector.rotateKey( token: testToken, keyName: registrarKey, }, + // key rotation currently not available using web-socket server + // web-socket connection not used to manages external keys + // user should re-enroll with new pub/priv key pair }, { enrollmentID: string; @@ -195,7 +221,16 @@ Currently Cactus Fabric Connector supports following Identity Providers - X509 : Simple and unsecured provider wherein `private` key is stored along with certificate in some `datastore`. Whenever connector require signature on fabric message, private key is brought from the `datastore` and message signed at the connector. - Vault-X.509 : Secure provider wherein `private` key is stored with vault's transit transit engine and certificate in `certDatastore`. Rather then bringing the key to the connector, message digest are sent to the vault server which returns the `signature`. -- WS-X.509 (Future Work) : Secure provider wherein `private` key is stored with `client` and certificate in `certDatastore`. To get the fabric messages signed, message digest is sent to the client via `webSocket` connection opened by the client in the beginning. +- WS-X.509 : Secure provider wherein `private` key is stored with `client` and certificate in `certDatastore`. To get the fabric messages signed, message digest is sent to the client via `webSocket` connection opened by the client in the beginning (as described above) + +### setting up a WS-X.509 provider +The following packages are used to access private keys (via web-socket) stored on a clients external device (e.g., browser, mobile app, or an IoT device...). + -[ws-identity](https://github.com/brioux/ws-identity): web-socket server that issues new ws-session tickets, authenticates incoming connections, and sends signature requests + -[ws-identity-client](https://github.com/brioux/ws-identity-client): backend connector to send requests from fabric application to ws-identity + -[ws-wallet](https://github.com/brioux/ws-wallet): external clients crypto key tool: create new key pair, request session ticket and open web-socket connection with ws-identity + +### Building the ws-identity docker image + ## Running the tests @@ -218,6 +253,7 @@ Build with a specific version of the npm package: DOCKER_BUILDKIT=1 docker build --build-arg NPM_PKG_VERSION=0.4.1 -f ./packages/cactus-plugin-ledger-connector-fabric/Dockerfile . -t cplcb ``` + #### Running the container Launch container with plugin configuration as an **environment variable**: diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index ea90428256..fa806576ee 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -92,7 +92,8 @@ "prom-client": "13.2.0", "temp": "0.9.4", "typescript-optional": "2.0.1", - "uuid": "8.3.2" + "uuid": "8.3.2", + "ws-identity-client": "1.0.2" }, "devDependencies": { "@hyperledger/cactus-plugin-keychain-memory": "1.0.0-rc.1", @@ -104,6 +105,7 @@ "@types/node-vault": "0.9.13", "@types/temp": "0.9.1", "@types/uuid": "8.3.1", - "fs-extra": "10.0.0" + "fs-extra": "10.0.0", + "ws-wallet": "1.1.5" } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json index a796b94849..9b74b37b09 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json @@ -75,11 +75,37 @@ }, "description": "vault key details for signing fabric message with private key stored with transit engine." }, + "WebSocketKey" : { + "type": "object", + "nullable": false, + "required": [ + "sessionId", + "signature" + ], + "properties": { + "sessionId": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false, + "description": "session Id to access client" + }, + "signature": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false, + "description": "signature of the session ID" + } + }, + "description": "web-socket key details for signing fabric message with private key stored with external client" + }, "FabricSigningCredentialType" : { "type": "string", "enum": [ "X.509", - "Vault-X.509" + "Vault-X.509", + "WS-X.509" ], "nullable": false, "description": "different type of identity provider for singing fabric messages supported by this package" @@ -110,6 +136,10 @@ "vaultTransitKey" : { "$ref" : "#/components/schemas/VaultTransitKey", "properties" : "vault key details , if Vault-X.509 identity provider to be used for singing fabric messages" + }, + "webSocketKey" : { + "$ref" : "#/components/schemas/WebSocketKey", + "properties" : "web-socket key details , if WS-X.509 identity provider to be used for singing fabric messages" } } }, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/create-gateway.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/create-gateway.ts index fb4ba76041..c6e28c2236 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/create-gateway.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/create-gateway.ts @@ -90,6 +90,20 @@ export async function createGateway( keyName: ctx.gatewayOptions.wallet.keychain.vaultTransitKey.keyName, }); break; + case FabricSigningCredentialType.WsX509: + if ( + !ctx.gatewayOptions.wallet.keychain || + !ctx.gatewayOptions.wallet.keychain.webSocketKey + ) { + throw new Error( + `require ctx.gatewayOptions.wallet.keychain.webSocketKey`, + ); + } + key = ctx.secureIdentity.getWebSocketKey({ + sessionId: ctx.gatewayOptions.wallet.keychain.webSocketKey.sessionId, + signature: ctx.gatewayOptions.wallet.keychain.webSocketKey.signature, + }); + break; case FabricSigningCredentialType.X509: key = ctx.secureIdentity.getDefaultKey({ private: certData.credentials.privateKey as string, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts index 22fea9cc69..f26a1541a9 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -551,6 +551,12 @@ export interface FabricSigningCredential { * @memberof FabricSigningCredential */ vaultTransitKey?: VaultTransitKey; + /** + * + * @type {WebSocketKey} + * @memberof FabricSigningCredential + */ + webSocketKey?: WebSocketKey; } /** * different type of identity provider for singing fabric messages supported by this package @@ -560,7 +566,8 @@ export interface FabricSigningCredential { export enum FabricSigningCredentialType { X509 = 'X.509', - VaultX509 = 'Vault-X.509' + VaultX509 = 'Vault-X.509', + WsX509 = 'WS-X.509' } /** @@ -979,6 +986,25 @@ export interface VaultTransitKey { */ token: string; } +/** + * web-socket key details for signing fabric message with private key stored with external client + * @export + * @interface WebSocketKey + */ +export interface WebSocketKey { + /** + * session Id to access client + * @type {string} + * @memberof WebSocketKey + */ + sessionId: string; + /** + * signature of the session ID + * @type {string} + * @memberof WebSocketKey + */ + signature: string; +} /** * DefaultApi - axios parameter creator diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts index 00dcb2914e..7e1266370d 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts @@ -1,8 +1,8 @@ import { IdentityProvider, IdentityData, Identity } from "fabric-network"; import { FabricSigningCredentialType } from "../generated/openapi/typescript-axios/api"; -import { Checks } from "@hyperledger/cactus-common"; import { ICryptoSuite, User, Utils, ICryptoKey } from "fabric-common"; import { + Checks, LogLevelDesc, Logger, LoggerProvider, @@ -10,18 +10,25 @@ import { import { Key } from "./internal/key"; import { InternalCryptoSuite } from "./internal/crypto-suite"; import { VaultTransitClient } from "./vault-client"; +import { WebSocketClient } from "./web-socket-client"; export interface IVaultConfig { endpoint: string; transitEngineMountPath: string; } +export interface IWebSocketConfig { + endpoint: string; + pathPrefix: string; + strictSSL?: boolean; +} + export interface ISecureIdentityProvidersOptions { activatedProviders: FabricSigningCredentialType[]; logLevel: LogLevelDesc; - // vault server config vaultConfig?: IVaultConfig; + webSocketConfig?: IWebSocketConfig; } export interface IIdentity extends Identity { @@ -37,6 +44,10 @@ export interface VaultKey { token: string; } +export interface WebSocketKey { + sessionId: string; + signature: string; +} export interface DefaultKey { // pem encoded private key private: string; @@ -70,6 +81,14 @@ export class SecureIdentityProviders implements IdentityProvider { ); this.log.debug(`${fnTag} Vault-X.509 identity provider activated`); } + if (opts.activatedProviders.includes(FabricSigningCredentialType.WsX509)) { + if (!opts.webSocketConfig) { + throw new Error(`${fnTag} require options.webSocketConfig`); + } + this.log.debug( + `${fnTag} WS-X.509 identity provider for host ${opts.webSocketConfig?.endpoint}${opts.webSocketConfig?.pathPrefix}`, //`, + ); + } this.defaultSuite = Utils.newCryptoSuite(); } @@ -115,11 +134,21 @@ export class SecureIdentityProviders implements IdentityProvider { }), ); } + getWebSocketKey(key: WebSocketKey): Key { + const client = new WebSocketClient({ + endpoint: this.opts.webSocketConfig?.endpoint as string, + pathPrefix: this.opts.webSocketConfig?.pathPrefix as string, + signature: key.signature, + sessionId: key.sessionId, + strictSSL: this.opts.webSocketConfig?.strictSSL, + logLevel: this.opts.logLevel, + }); + return new Key(`sessionId: ${key.sessionId}`, client); + } getDefaultKey(key: DefaultKey): ICryptoKey { return this.defaultSuite.createKeyFromRaw(key.private); } - // not required things readonly type = ""; getCryptoSuite(): ICryptoSuite { diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/web-socket-client.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/web-socket-client.ts new file mode 100644 index 0000000000..c8f48d1c75 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/web-socket-client.ts @@ -0,0 +1,126 @@ +import { + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { KJUR, KEYUTIL } from "jsrsasign"; +import { InternalIdentityClient, ISignatureResponse } from "./internal/client"; +import { ECCurveType } from "./internal/crypto-util"; +import { WsIdentityClient } from "ws-identity-client"; + +export interface WSClientOptions { + // full url of web-socket identity server + // eg : http://localhost:8700 + endpoint: string; + + // pathPrefix for incoming web-socket connections + // eg : /sessions + pathPrefix: string; + + // websocket sessionId assigned to the client + sessionId: string; + // signature of the sessionId by the client + signature: string; + + logLevel?: LogLevelDesc; + strictSSL?: boolean; +} + +export class WebSocketClient implements InternalIdentityClient { + public readonly className = "WebSocketClient"; + private readonly log: Logger; + private readonly backend: WsIdentityClient; + private pubKeyEcdsa?: KJUR.crypto.ECDSA; + private curve?: ECCurveType; + + constructor(opts: WSClientOptions) { + this.log = LoggerProvider.getOrCreate({ + label: "WebSocketClient", + level: opts.logLevel || "INFO", + }); + this.backend = new WsIdentityClient({ + signature: opts.signature, + sessionId: opts.sessionId, + endpoint: opts.endpoint, + pathPrefix: opts.pathPrefix, + apiVersion: "v1", + rpDefaults: { + strictSSL: opts.strictSSL !== false, + }, + }); + } + + /** + * @description : sign message and return in a format that fabric understand + * @param keyName : required by the sign method of abstract InternalIdentityClient + * serves no role in the web-socket communication + * the client only knows that a web-socket connection has been established + * for a unique sessionId assigned to a given public key + * @param digest to be singed + */ + async sign(keyName: string, digest: Buffer): Promise { + const fnTag = `${this.className}#sign`; + this.log.debug( + `${fnTag} send digest for pub-key ${keyName}: digest-size = ${digest.length}`, + ); + const resp = await this.backend.write( + "sign", + { digest: digest.toString("base64") }, + {}, + ); + this.log.debug(`${fnTag} response from web-socket server : %o`, resp); + if (!this.curve) { + await this.getPub(keyName); + } + if (resp) { + return { + sig: Buffer.from(resp, "base64"), + crv: this.curve as ECCurveType, + }; + } + throw new Error( + `invalid response from ws-identity-client ${JSON.stringify(resp)}`, + ); + } + + /** + * @description return the pre-built ECDSA public key object + */ + async getPub(keyName: string): Promise { + const fnTag = `${this.className}#get-pub`; + try { + this.log.debug( + `${fnTag} return the ECDSA public key object of the connected client. ` + + `keyName (${keyName}) is required but not used here (client cannot access other keys)`, + ); + if (!this.pubKeyEcdsa) { + this.log.debug(`${fnTag} set the pub-key-ecdsa object for ${keyName}`); + const resp = await this.backend.read("get-pub", {}); + this.pubKeyEcdsa = KEYUTIL.getKey(resp) as KJUR.crypto.ECDSA; + this.curve = ECCurveType.P256; + if ((this.pubKeyEcdsa as any).curveName === "secp384r1") { + this.curve = ECCurveType.P384; + } + } + return this.pubKeyEcdsa as KJUR.crypto.ECDSA; + } catch (error) { + throw new Error( + `failed to retrieve pub-key-ecdsa from ws-identity server`, + ); + } + } + /** + * @description Rotate public used by client with keyName + * this method is inactive when using a web-socket client + * not authorized to request or change external keys + */ + async rotateKey(keyName: string): Promise { + const fnTag = `${this.className}#rotate-key`; + this.log.debug( + `${fnTag} inactive method for ${this.className}, provide key-name ${keyName} as abstract interface requirement`, + ); + throw new Error( + "web-socket client can not rotate private keys. External client must enroll with a new csr", + ); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts index 8262281741..8c4cc92f2e 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts @@ -95,6 +95,7 @@ import { createGateway } from "./common/create-gateway"; import { Endorser, ICryptoKey } from "fabric-common"; import { IVaultConfig, + IWebSocketConfig, SecureIdentityProviders, IIdentity, } from "./identity/identity-provider"; @@ -136,6 +137,7 @@ export interface IPluginLedgerConnectorFabricOptions eventHandlerOptions?: GatewayEventHandlerOptions; supportedIdentity?: FabricSigningCredentialType[]; vaultConfig?: IVaultConfig; + webSocketConfig?: IWebSocketConfig; } export class PluginLedgerConnectorFabric @@ -202,6 +204,7 @@ export class PluginLedgerConnectorFabric ], logLevel: opts.logLevel || "INFO", vaultConfig: opts.vaultConfig, + webSocketConfig: opts.webSocketConfig, }); this.certStore = new CertDatastore(opts.pluginRegistry); } @@ -895,6 +898,15 @@ export class PluginLedgerConnectorFabric keyName: signingCredential.vaultTransitKey.keyName, }); break; + case FabricSigningCredentialType.WsX509: + if (!signingCredential.webSocketKey) { + throw new Error(`require signingCredential.webSocketKey`); + } + key = this.secureIdentity.getWebSocketKey({ + sessionId: signingCredential.webSocketKey.sessionId, + signature: signingCredential.webSocketKey.signature, + }); + break; case FabricSigningCredentialType.X509: key = this.secureIdentity.getDefaultKey({ private: certData.credentials.privateKey as string, @@ -1169,17 +1181,28 @@ export class PluginLedgerConnectorFabric enrollmentID: request.enrollmentID, enrollmentSecret: request.enrollmentSecret, }; + let key; switch (iType) { case FabricSigningCredentialType.VaultX509: if (!identity.vaultTransitKey) { throw new Error(`${fnTag} require identity.vaultTransitKey`); } - const key = this.secureIdentity.getVaultKey({ + key = this.secureIdentity.getVaultKey({ token: identity.vaultTransitKey.token, keyName: identity.vaultTransitKey.keyName, }); enrollmentRequest.csr = await key.generateCSR(request.enrollmentID); break; + case FabricSigningCredentialType.WsX509: + if (!identity.webSocketKey) { + throw new Error(`${fnTag} require identity.webSocketKey`); + } + key = this.secureIdentity.getWebSocketKey({ + sessionId: identity.webSocketKey.sessionId, + signature: identity.webSocketKey.signature, + }); + enrollmentRequest.csr = await key.generateCSR(request.enrollmentID); + break; } const resp = await ca.enroll(enrollmentRequest); const certData: IIdentityData = { @@ -1242,6 +1265,15 @@ export class PluginLedgerConnectorFabric keyName: registrar.vaultTransitKey.keyName, }); break; + case FabricSigningCredentialType.WsX509: + if (!registrar.webSocketKey) { + throw new Error(`${fnTag} require registrar.webSocketKey`); + } + key = this.secureIdentity.getWebSocketKey({ + sessionId: registrar.webSocketKey.sessionId, + signature: registrar.webSocketKey.signature, + }); + break; default: throw new Error(`${fnTag} UNRECOGNIZED_IDENTITY_TYPE type = ${iType}`); } @@ -1301,6 +1333,11 @@ export class PluginLedgerConnectorFabric }); await key.rotate(); break; + case FabricSigningCredentialType.WsX509: + throw new Error( + `${fnTag} web socket is not setup to rotate keys. Client should enroll with a new key)`, + ); + break; } identity.type = iType; await this.enroll(identity, { diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts index e15a92a081..fe1dc46a88 100755 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts @@ -23,5 +23,5 @@ export { ICompilationResult, } from "./chain-code-compiler"; -export { IVaultConfig } from "./identity/identity-provider"; +export { IVaultConfig, IWebSocketConfig } from "./identity/identity-provider"; export { IIdentityData } from "./identity/internal/cert-datastore"; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts new file mode 100644 index 0000000000..9d78b8fbba --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts @@ -0,0 +1,322 @@ +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import test, { Test } from "tape-promise/tape"; +import { IPluginLedgerConnectorFabricOptions } from "../../../../main/typescript/plugin-ledger-connector-fabric"; +import { v4 as uuidv4 } from "uuid"; +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + DefaultEventHandlerStrategy, + FabricSigningCredentialType, + IWebSocketConfig, + PluginLedgerConnectorFabric, + IIdentityData, + FabricContractInvocationType, +} from "../../../../main/typescript/public-api"; +import { DiscoveryOptions } from "fabric-network"; + +const logLevel: LogLevelDesc = "ERROR"; +import { + Containers, + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, + WsTestServer, + WS_IDENTITY_HTTP_PORT, +} from "@hyperledger/cactus-test-tooling"; +import { v4 as internalIpV4 } from "internal-ip"; +import { WsWallet } from "ws-wallet"; +import { WsIdentityClient } from "ws-identity-client"; + +// test scenario +// - enroll registrar (both using default identity and webSocket(p256) identity) +// - register 2 client (using registrar identity) +// - enroll 1st client using default identity +// - enroll 2nd client using web socket identity(p384) +// - make invoke (InitLedger) using 1st client +// - make invoke (TransferAsset) using 2nd client (p384) client +// - make query ("ReadAsset") using registrar(p256) +test("run-transaction-with-ws-ids", async (t: Test) => { + test.onFailure(async () => { + await Containers.logDiagnostics({ logLevel }); + }); + + const ledger = new FabricTestLedgerV1({ + emitContainerLogs: true, + publishAllPorts: true, + imageName: "ghcr.io/hyperledger/cactus-fabric2-all-in-one", + imageVersion: "2021-09-02--fix-876-supervisord-retries", + envVars: new Map([["FABRIC_VERSION", "2.2.0"]]), + logLevel, + }); + + test.onFinish(async () => { + await ledger.stop(); + await ledger.destroy(); + await pruneDockerAllIfGithubAction({ logLevel }); + }); + + const wsTestContainer = new WsTestServer({}); + await wsTestContainer.start(); + await ledger.start(); + + const connectionProfile = await ledger.getConnectionProfileOrg1(); + t.ok(connectionProfile, "getConnectionProfileOrg1() out truthy OK"); + + const registrarKey = "registrar"; + const client2Key = "client-ws"; + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + + const ci = await Containers.getById(wsTestContainer.containerId); + const wsIpAddr = await internalIpV4(); + const hostPort = await Containers.getPublicPort(WS_IDENTITY_HTTP_PORT, ci); + + const wsUrl = `http://${wsIpAddr}:${hostPort}`; + + const wsConfig: IWebSocketConfig = { + endpoint: wsUrl, + pathPrefix: "/identity", + }; + + // external web-socket client + const wsAdmin = new WsWallet({ + keyName: "admin", + logLevel, + strictSSL: false, + }); + + // external web-socket client + const wsUser = new WsWallet({ + keyName: "user", + logLevel, + strictSSL: false, + }); + + test.onFinish(async () => { + await wsTestContainer.stop(); + await wsTestContainer.destroy(); + await wsAdmin.close(); + await wsUser.close(); + }); + + /// + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId: keychainId, + logLevel, + }); + + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); + + const discoveryOptions: DiscoveryOptions = { + enabled: true, + asLocalhost: true, + }; + const supportedIdentity: FabricSigningCredentialType[] = [ + FabricSigningCredentialType.WsX509, + FabricSigningCredentialType.X509, + ]; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: uuidv4(), + pluginRegistry, + sshConfig: {}, + cliContainerEnv: {}, + peerBinary: "not-required", + logLevel, + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + supportedIdentity, + webSocketConfig: wsConfig, + }; + + const plugin = new PluginLedgerConnectorFabric(pluginOptions); + + const wsIdClient = new WsIdentityClient({ + apiVersion: "v1", + endpoint: wsUrl, + rpDefaults: { + strictSSL: false, + }, + }); + + t.test("with-webSocketKey", async (t: Test) => { + let webSocketKeyAdmin, webSocketKeyUser; + { + const { sessionId, url } = JSON.parse( + await wsIdClient.write( + "session/new", + { + pubKeyHex: wsAdmin.getPubKeyHex(), + keyName: wsAdmin.keyName, + }, + {}, + ), + ); + webSocketKeyAdmin = await wsAdmin.open(sessionId, url); + } + + { + // enroll registrar using ws identity + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: registrarKey + "-ws", + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyAdmin, + }, + { + enrollmentID: "admin", + enrollmentSecret: "adminpw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); + const rawCert = await keychainPlugin.get(registrarKey + "-ws"); + t.ok(rawCert); + const certData = JSON.parse(rawCert) as IIdentityData; + t.equal(certData.type, FabricSigningCredentialType.WsX509); + t.notok(certData.credentials.privateKey); + } + { + // register a client using registrar's ws identity + const secret = await plugin.register( + { + keychainId: keychainId, + keychainRef: registrarKey + "-ws", + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyAdmin, + }, + { + enrollmentID: client2Key, + enrollmentSecret: "pw", + affiliation: "org1.department1", + }, + "ca.org1.example.com", + ); + t.equal(secret, "pw"); + } + { + const { sessionId, url } = JSON.parse( + await wsIdClient.write( + "session/new", + { + pubKeyHex: wsUser.getPubKeyHex(), + keyName: wsUser.keyName, + }, + {}, + ), + ); + webSocketKeyUser = await wsUser.open(sessionId, url); + } + { + // enroll client2 using ws identity + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: client2Key, + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyUser, + }, + { + enrollmentID: client2Key, + enrollmentSecret: "pw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); + const rawCert = await keychainPlugin.get(client2Key); + t.ok(rawCert, "rawCert truthy OK"); + const { type, credentials } = JSON.parse(rawCert) as IIdentityData; + const { privateKey } = credentials; + t.equal(type, FabricSigningCredentialType.WsX509, "Cert is X509 OK"); + t.notok(privateKey, "certData.credentials.privateKey falsy OK"); + } + // Temporary workaround here: Deploy a second contract because the default + // one is being hammered with "InitLedger" transactions by the container's + // own healthcheck (see healthcheck.sh in the fabric-all-in-one folder). + // The above makes it so that transactions are triggering multiversion + // concurrency control errors. + // Deploying a fresh new contract here as a quick workaround resolves that + // problem, the real fix is to make the health check use a tx that does not + // commit instead just reads something which should still prove that the + // AIO legder is up and running fine but it won't cause this issue anymore. + const contractName = "basic2"; + const cmd = [ + "./network.sh", + "deployCC", + "-ccn", + contractName, + "-ccp", + "../asset-transfer-basic/chaincode-go", + "-ccl", + "go", + ]; + + const container = ledger.getContainer(); + const timeout = 180000; // 3 minutes + const cwd = "/fabric-samples/test-network/"; + const out = await Containers.exec(container, cmd, timeout, logLevel, cwd); + t.ok(out, "deploy Basic2 command output truthy OK"); + t.comment("Output of Basic2 contract deployment below:"); + t.comment(out); + + { + // make invoke InitLedger using a client1 client + const resp = await plugin.transact({ + signingCredential: { + keychainId: keychainId, + keychainRef: client2Key, + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyUser, + }, + channelName: "mychannel", + contractName, + invocationType: FabricContractInvocationType.Send, + methodName: "InitLedger", + params: [], + }); + t.true(resp.success, "InitLedger tx for Basic2 success===true OK"); + } + { + // make invoke TransferAsset using a client2 client + const resp = await plugin.transact({ + signingCredential: { + keychainId: keychainId, + keychainRef: client2Key, + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyUser, + }, + channelName: "mychannel", + contractName, + invocationType: FabricContractInvocationType.Send, + methodName: "TransferAsset", + params: ["asset1", "client2"], + }); + t.true(resp.success, "TransferAsset asset1 client2 success true OK"); + } + { + const resp = await plugin.transact({ + signingCredential: { + keychainId: keychainId, + keychainRef: registrarKey + "-ws", + type: FabricSigningCredentialType.WsX509, + webSocketKey: webSocketKeyAdmin, + }, + channelName: "mychannel", + contractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + }); + t.true(resp.success); + const asset = JSON.parse(resp.functionOutput); + t.equal(asset.owner, "client2"); + } + t.end(); + }); + t.end(); +}); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts index 3d1b5513f6..c51f0d9c0f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts @@ -4,16 +4,21 @@ import { Containers, VaultTestServer, K_DEFAULT_VAULT_HTTP_PORT, + WsTestServer, + WS_IDENTITY_HTTP_PORT, } from "@hyperledger/cactus-test-tooling"; import test, { Test } from "tape-promise/tape"; import { InternalIdentityClient } from "../../../main/typescript/identity/internal/client"; import { VaultTransitClient } from "../../../main/typescript/identity/vault-client"; +import { WebSocketClient } from "../../../main/typescript/identity/web-socket-client"; import { LogLevelDesc } from "@hyperledger/cactus-common"; import { createHash } from "crypto"; import { ECCurveType } from "../../../main/typescript/identity/internal/crypto-util"; import { KJUR } from "jsrsasign"; +import { WsWallet, ECCurveType as ECCurveTypeW } from "ws-wallet"; +import { WsIdentityClient } from "ws-identity-client"; -const logLevel: LogLevelDesc = "TRACE"; +const logLevel: LogLevelDesc = "ERROR"; // a generic test suite for testing all the identity clients // supported by this package test("identity-clients", async (t: Test) => { @@ -22,24 +27,59 @@ test("identity-clients", async (t: Test) => { const testECP384 = "test-ec-p384"; const testNotFoundKey = "keyNotFound"; { + const IpAdd = await internalIpV4(); + // + // setup web-socket client + const wsTestContainer = new WsTestServer({ + logLevel, + imageVersion: "0.0.1", + }); + await wsTestContainer.start(); + let ci = await Containers.getById(wsTestContainer.containerId); + const wsHostPort = await Containers.getPublicPort( + WS_IDENTITY_HTTP_PORT, + ci, + ); // setup vault client const vaultTestContainer = new VaultTestServer({}); await vaultTestContainer.start(); - - const ci = await Containers.getById(vaultTestContainer.containerId); - const vaultIpAddr = await internalIpV4(); + ci = await Containers.getById(vaultTestContainer.containerId); const hostPort = await Containers.getPublicPort( K_DEFAULT_VAULT_HTTP_PORT, ci, ); - const vaultHost = `http://${vaultIpAddr}:${hostPort}`; + + const vaultHost = `http://${IpAdd}:${hostPort}`; + const wsUrl = `http://${IpAdd}:${wsHostPort}`; + + // External client with private key + const wsWallet256 = new WsWallet({ + keyName: "256", + logLevel, + strictSSL: false, + }); + + // establish session Id to be used by external client with p384 key + const wsWallet384 = new WsWallet({ + keyName: "384", + curve: "p384" as ECCurveTypeW, + logLevel, + strictSSL: false, + }); + test.onFinish(async () => { + await wsTestContainer.stop(); + await wsTestContainer.destroy(); await vaultTestContainer.stop(); await vaultTestContainer.destroy(); + await wsWallet384.close(); + await wsWallet256.close(); }); + const mountPath = "/transit"; const testToken = "myroot"; // mount transit secret engine + await axios.post( vaultHost + "/v1/sys/mounts" + mountPath, { @@ -82,13 +122,80 @@ test("identity-clients", async (t: Test) => { logLevel: logLevel, }), ); + + const wsIdClient = new WsIdentityClient({ + apiVersion: "v1", + endpoint: wsUrl, + rpDefaults: { + strictSSL: false, + }, + }); + + const wsPathPrefix = "/identity"; + { + const newSidResp = JSON.parse( + await wsIdClient.write( + "session/new", + { + pubKeyHex: wsWallet256.getPubKeyHex(), + keyName: wsWallet256.keyName, + }, + {}, + ), + ); + const { signature, sessionId } = await wsWallet256.open( + newSidResp.sessionId, + newSidResp.url, + ); + + testClients.set( + "web-socket-client-256", + new WebSocketClient({ + endpoint: wsUrl, + pathPrefix: wsPathPrefix, + signature, + sessionId, + logLevel, + }), + ); + } + { + const newSidResp = JSON.parse( + await wsIdClient.write( + "session/new", + { + pubKeyHex: wsWallet384.getPubKeyHex(), + keyName: wsWallet384.keyName, + }, + {}, + ), + ); + const { signature, sessionId } = await wsWallet384.open( + newSidResp.sessionId, + newSidResp.url, + ); + testClients.set( + "web-socket-client-384", + new WebSocketClient({ + endpoint: wsUrl, + pathPrefix: wsPathPrefix, + signature, + sessionId, + logLevel, + }), + ); + } } + // for (const [clientName, client] of testClients) { const digest = Buffer.from("Hello Cactus"); const hashDigest = createHash("sha256").update(digest).digest(); t.test(`${clientName}::sign`, async (t: Test) => { - { + if ( + clientName == "web-socket-client-256" || + clientName == "vault-client" + ) { const { sig, crv } = await client.sign(testECP256, hashDigest); t.equal(crv, ECCurveType.P256); t.ok(sig); @@ -111,7 +218,10 @@ test("identity-clients", async (t: Test) => { t.true(verify.verify(sig.toString("hex"))); } } - { + if ( + clientName == "web-socket-client-384" || + clientName == "vault-client" + ) { const { sig, crv } = await client.sign(testECP384, hashDigest); t.equal(crv, ECCurveType.P384); t.ok(sig); @@ -137,17 +247,23 @@ test("identity-clients", async (t: Test) => { t.end(); }); t.test(`${clientName}::getPub`, async (t: Test) => { - { + if ( + clientName == "web-socket-client-256" || + clientName == "vault-client" + ) { const pub = await client.getPub(testECP256); t.ok(pub); t.equal((pub as any).curveName, "secp256r1"); } - { + if ( + //clientName == "web-socket-client-384" || + clientName == "vault-client" + ) { const pub = await client.getPub(testECP384); t.ok(pub); t.equal((pub as any).curveName, "secp384r1"); } - { + if (clientName == "vault-client") { try { await client.getPub(testNotFoundKey); t.fail("Should not get here"); @@ -162,7 +278,7 @@ test("identity-clients", async (t: Test) => { t.end(); }); t.test(`${clientName}::rotateKey`, async (t: Test) => { - { + if (clientName == "vault-client") { const pubOld = await client.getPub(testECP256); await client.rotateKey(testECP256); const pubNew = await client.getPub(testECP256); @@ -179,7 +295,7 @@ test("identity-clients", async (t: Test) => { t.false(verify.verify(sig.toString("hex"))); } } - { + if (clientName == "vault-client") { const pubOld = await client.getPub(testECP384); await client.rotateKey(testECP384); const pubNew = await client.getPub(testECP384); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts index ab4ff9338a..5cbb3f2c9a 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts @@ -69,10 +69,8 @@ GfNTZZQn1F9PQCxOequ4XS4XFmng3MD8jkP58Sak/6QaXYvqAEB6pBT/gA== "base64", ); const csrPem = CryptoUtil.getPemCSR(csr, signature); - // console.log(KJUR.asn1.csr.CSRUtil); { const csr = KJ.asn1.csr.CSRUtil.getParam(csrPem); - console.log(csr); t.equal("/CN=Cactus", csr.subject.str); } t.end(); diff --git a/packages/cactus-test-tooling/README.md b/packages/cactus-test-tooling/README.md index bff295b7bf..4d59df6304 100644 --- a/packages/cactus-test-tooling/README.md +++ b/packages/cactus-test-tooling/README.md @@ -7,3 +7,15 @@ ``` // TODO: DEMONSTRATE API ``` +## Docker image for the ws-identity server + +A docker image of the [ws-identity server](https://hub.docker.com/repository/docker/brioux/ws-identity) is used to test integration of WS-X.509 credential type in the fabric connector plugin. + +[ws-identity](https://github.com/brioux/ws-identity) includes A Docker file to build the image: +clone the repo, install packages, build src and the image +``` +npm install +npm run build +docker build . -t [image-name] +``` + diff --git a/packages/cactus-test-tooling/package.json b/packages/cactus-test-tooling/package.json index 25ce8b1cee..1475d663a3 100644 --- a/packages/cactus-test-tooling/package.json +++ b/packages/cactus-test-tooling/package.json @@ -74,6 +74,7 @@ "axios": "0.21.4", "compare-versions": "3.6.0", "dockerode": "3.3.0", + "elliptic": "6.5.4", "execa": "5.1.1", "fabric-ca-client": "2.2.8", "fabric-network": "2.2.8", diff --git a/packages/cactus-test-tooling/src/main/typescript/public-api.ts b/packages/cactus-test-tooling/src/main/typescript/public-api.ts index 94df89df75..b3c25ee9c9 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -81,6 +81,12 @@ export { K_DEFAULT_VAULT_DEV_ROOT_TOKEN, } from "./vault-test-server/vault-test-server"; +export { + IWsTestServerOptions, + WsTestServer, + WS_IDENTITY_HTTP_PORT, +} from "./ws-test-server/ws-test-server"; + export { ILocalStackContainerOptions, LocalStackContainer, diff --git a/packages/cactus-test-tooling/src/main/typescript/ws-test-server/ws-test-server.ts b/packages/cactus-test-tooling/src/main/typescript/ws-test-server/ws-test-server.ts new file mode 100644 index 0000000000..f7e0074660 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/ws-test-server/ws-test-server.ts @@ -0,0 +1,153 @@ +import { EventEmitter } from "events"; + +import Docker, { Container } from "dockerode"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; + +import { Containers } from "../common/containers"; + +export const WS_IDENTITY_IMAGE_NAME = `ghcr.io/brioux/ws-identity`; +export const WS_IDENTITY_IMAGE_VERSION = "0.0.1"; +export const WS_IDENTITY_HTTP_PORT = 8700; + +export interface IWsTestServerOptions { + envVars?: string[]; + imageVersion?: string; + imageName?: string; + logLevel?: LogLevelDesc; +} + +/** + * > **Do not use this image in production.** + * + * Class responsible for programmatically managing a container that is running + * ws-identity server + */ +export class WsTestServer { + public static readonly CLASS_NAME = "WsTestServer"; + + private readonly log: Logger; + private readonly imageName: string; + private readonly imageVersion: string; + private readonly envVars: string[]; + + private _container: Container | undefined; + private _containerId: string | undefined; + + public get containerId(): string { + Checks.nonBlankString(this._containerId, `${this.className}:_containerId`); + return this._containerId as string; + } + + public get container(): Container { + Checks.nonBlankString(this._container, `${this.className}:_container`); + return this._container as Container; + } + + public get className(): string { + return WsTestServer.CLASS_NAME; + } + + public get imageFqn(): string { + return `${this.imageName}:${this.imageVersion}`; + } + + constructor(public readonly options: IWsTestServerOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + + this.imageName = this.options.imageName || WS_IDENTITY_IMAGE_NAME; + this.imageVersion = this.options.imageVersion || WS_IDENTITY_IMAGE_VERSION; + this.envVars = this.options.envVars || []; + + this.log.info(`Created ${this.className} OK. Image FQN: ${this.imageFqn}`); + } + + public async start(): Promise { + if (this._container) { + await this._container.stop(); + await this._container.remove(); + } + const docker = new Docker(); + + await Containers.pullImage(this.imageFqn, {}, this.options.logLevel); + + return new Promise((resolve, reject) => { + const eventEmitter: EventEmitter = docker.run( + this.imageFqn, + [], + [], + { + ExposedPorts: { + [`${WS_IDENTITY_HTTP_PORT}/tcp`]: {}, + }, + // This is a workaround needed for macOS which has issues with routing + // to docker container's IP addresses directly... + // https://stackoverflow.com/a/39217691 + PublishAllPorts: true, + /*Env: [`WS_IDENTITY_PATH=${WS_IDENTITY_PATH}`, ...this.envVars], + HostConfig: { + // NetworkMode: "host", + CapAdd: ["IPC_LOCK"], + PublishAllPorts: true, + },*/ + //Healthcheck: { + // Test: ["CMD-SHELL", "wget -O- http://127.0.0.1:8200/v1/sys/health"], + // Interval: 100 * 1000000, + //}, + }, + {}, + (err: Error) => { + if (err) { + reject(err); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + this._container = container; + this._containerId = container.id; + resolve(container); + try { + await Containers.waitForHealthCheck(this._containerId); + resolve(container); + } catch (ex) { + reject(ex); + } + }); + }); + } + + public async stop(): Promise { + await Containers.stop(this._container as Docker.Container); + } + + public destroy(): Promise { + const fnTag = `${this.className}#destroy()`; + if (this._container) { + return this._container.remove(); + } else { + const ex = new Error(`${fnTag} Container not found, nothing to destroy.`); + return Promise.reject(ex); + } + } + + public async getHostPortHttp(): Promise { + const fnTag = `${this.className}#getHostPortHttp()`; + if (this._containerId) { + const cInfo = await Containers.getById(this._containerId); + return Containers.getPublicPort(WS_IDENTITY_HTTP_PORT, cInfo); + } else { + throw new Error(`${fnTag} Container ID not set. Did you call start()?`); + } + } +}