From 2161e0d75bac49654f0d38c8a9e2b03234894ed8 Mon Sep 17 00:00:00 2001 From: Pritam Singh Date: Wed, 25 Aug 2021 00:33:34 +0530 Subject: [PATCH] feat(connector-fabric): add support for vault transit secret engine Signed-off-by: Pritam Singh --- .cspell.json | 6 + .../README.md | 128 ++++++ .../run-transaction-endpoint-enroll.png | Bin 0 -> 73039 bytes .../run-transaction-endpoint-enroll.puml | 42 ++ .../package.json | 11 +- .../src/main/json/openapi.json | 42 ++ .../main/typescript/common/create-gateway.ts | 75 +++- .../generated/openapi/typescript-axios/api.ts | 41 ++ .../typescript/identity/identity-provider.ts | 134 +++++++ .../identity/internal/cert-datastore.ts | 37 ++ .../typescript/identity/internal/client.ts | 31 ++ .../identity/internal/crypto-suite.ts | 45 +++ .../identity/internal/crypto-util.ts | 87 ++++ .../main/typescript/identity/internal/key.ts | 54 +++ .../main/typescript/identity/vault-client.ts | 117 ++++++ .../plugin-ledger-connector-fabric.ts | 261 ++++++++++-- .../src/main/typescript/public-api.ts | 3 + .../run-transaction-with-identities.test.ts | 371 ++++++++++++++++++ .../integration/identity-client.test.ts | 203 ++++++++++ .../identity-internal-crypto-utils.test.ts | 81 ++++ .../src/main/typescript/common/containers.ts | 9 +- yarn.lock | 97 ++++- 22 files changed, 1833 insertions(+), 42 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-fabric/docs/architecture/images/run-transaction-endpoint-enroll.png create mode 100644 packages/cactus-plugin-ledger-connector-fabric/docs/architecture/run-transaction-endpoint-enroll.puml create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/cert-datastore.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/client.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-suite.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-util.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/key.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/vault-client.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts diff --git a/.cspell.json b/.cspell.json index a74cd44451..babc8ef66e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -25,6 +25,7 @@ "DHTAPI", "DockerOde", "ealen", + "ecparams", "Errorf", "escc", "execa", @@ -49,8 +50,11 @@ "isready", "jboss", "JORDI", + "jsrsasign", "Keychain", "Keycloak", + "KEYUTIL", + "KJUR", "Knetic", "LEDGERBLOCKACK", "lmify", @@ -85,7 +89,9 @@ "protoc", "protos", "RUSTC", + "sbjpubkey", "Secp", + "shrn", "socketio", "SPDX", "Sprintf", diff --git a/packages/cactus-plugin-ledger-connector-fabric/README.md b/packages/cactus-plugin-ledger-connector-fabric/README.md index e5232f8a14..fe8e099c8f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/README.md +++ b/packages/cactus-plugin-ledger-connector-fabric/README.md @@ -10,6 +10,7 @@ This plugin provides `Cactus` a way to interact with Fabric networks. Using this - [Getting Started](#getting-started) - [Architecture](#architecture) - [Usage](#usage) + - [Identity Providers](#identity-providers) - [Runing the tests](#running-the-tests) - [Built With](#built-with) - [Prometheus Exporter](#prometheus-exporter) @@ -43,6 +44,10 @@ The above diagram shows the sequence diagram of run-transaction-endpoint. User A ![run-transaction-endpoint transact() method](docs/architecture/images/run-transaction-endpoint-transact.png) The above diagram shows the sequence diagraom of transact() method of the PluginLedgerConnectorFabric class. The caller to this function, which in reference to the above sequence diagram is API server, sends RunTransactionRequest object as an argument to the transact() method. Based on the invocationType (FabricContractInvocationType.CALL, FabricCOntractInvocationType.SEND), corresponding responses are send back to the caller. +![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. + ## Usage To use this import public-api and create new **PluginLedgerConnectorFabric** and **ChainCodeCompiler**. @@ -66,8 +71,131 @@ For compile the chaincodes: const result = await compiler.compile(opts); ``` +To support signing of message with multiple identity types +```typescript +// vault server config for supporting vault identity provider +const vaultConfig:IVaultConfig = { + endpoint : "http://localhost:8200", + transitEngineMountPath: "/transit", +} +// provide list of identity signing to be supported +const supportedIdentity:FabricSigningCredentialType[] = [FabricSigningCredentialType.VaultX509,FabricSigningCredentialType.X509] +const pluginOptions:IPluginLedgerConnectorFabricOptions = { + // other options + vaultConfig : vaultConfig, + 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 + vaultTransitKey: { + token: "vault-token", + keyName: "vault-key-label", + }, + }, + { + enrollmentID: "client2", + enrollmentSecret: "pw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); +``` +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 + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + { + enrollmentID: "client-enrollmentID", + enrollmentSecret: "pw", + affiliation: "org1.department1", + }, + "ca.org1.example.com", // caID + ); +``` + +To transact with fabric +```typescript +const resp = await connector.transact{ + signingCredential: { + keychainId: keychainId, + keychainRef: "client-certData-id", + type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 + + // require in case of vault + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + // .. other options +} +``` + +To Rotate the key +```typescript +await connector.rotateKey( + { + keychainId: keychainId, + keychainRef: "client-certData-id", + type: FabricSigningCredentialType.VaultX509, // FabricSigningCredentialType.X509 + + // require in case of vault + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + { + enrollmentID: string; + enrollmentSecret: string; + caId: string; + } +) +``` > Extensive documentation and examples in the [readthedocs](https://readthedocs.org/projects/hyperledger-cactus/) (WIP) +## Identity Providers + +Identity providers allows client to manage their private more effectively and securely. Cactus Fabric Connector support multiple type of providers. Each provider differ based upon where the private are stored. On High level certificate credential are stored as + +```typescript +{ + type: FabricSigningCredentialType; + credentials: { + certificate: string; + // if identity type is IdentityProvidersType.X509 + privateKey?: string; + }; + mspId: string; +} +``` + +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. ## Running the tests diff --git a/packages/cactus-plugin-ledger-connector-fabric/docs/architecture/images/run-transaction-endpoint-enroll.png b/packages/cactus-plugin-ledger-connector-fabric/docs/architecture/images/run-transaction-endpoint-enroll.png new file mode 100644 index 0000000000000000000000000000000000000000..76310b39e2e79c6c3e25a610d87235e012076740 GIT binary patch literal 73039 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 c{^2lzRz-x-=gfnBs>GivO6r#i6mCBFUjb%7 literal 0 HcmV?d00001 diff --git a/packages/cactus-plugin-ledger-connector-fabric/docs/architecture/run-transaction-endpoint-enroll.puml b/packages/cactus-plugin-ledger-connector-fabric/docs/architecture/run-transaction-endpoint-enroll.puml new file mode 100644 index 0000000000..934e73f7ed --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/docs/architecture/run-transaction-endpoint-enroll.puml @@ -0,0 +1,42 @@ +@startuml +title Hyperledger Cactus\nSequence Diagram\nRun Transaction Endpoint\enroll() method + +skinparam sequenceArrowThickness 2 +skinparam roundcorner 20 +skinparam maxmessagesize 300 +skinparam sequenceParticipant underline + +actor "Caller" as caller +participant "PluginLedgerConnectorFabric" as t << (C,#ADD1B2) class >> + +autoactivate on + +activate caller +caller -> t : enroll(signer: FabricSigningCredential,\nreq: FabricEnrollmentRequest) +t->t: {\n\tenrollmentID: string,\n\tenrollmentSecret: string,\n\tcaId: string,\n\tmspId: string\n} + +alt #LightBlue this.activatedSinger.includes(singer.type) === false + t-->caller: throw Error(`singer.type not activated`) +end + +t->t : ca = this.createCaClient(caId) +t->t : enrollmentRequest = {enrollmentID,enrollmentSecret} + +group #LightBlue if singer.type == FabricSigningCredentialType.DeafultX509 +else #LightYellow if singer.type == FabricSigningCredentialType.VaulttX509 + t->t : enrollmentRequest.csr = await (this.activatedSinger[VaulttX509] as VaultX509Provider)\n\t.getKey(singer.vaultTransitKey)\n\t.generateCSR(enrollmentID) +else #LightCoral default + t -> caller: throw Error('unknown SingerType') +end + +t->t: resp = await ca.enroll(enrollmentRequest) +t->t: certData = {singer.type,mspId,certificate} + +alt if resp.key !== undefined + t->t: resp.credentials.privateKey = resp.key.toBytes(); +end + +t->t : await certDatastore.put(singer.keychainref,certData) + +deactivate caller +@enduml \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index d6d8685668..745f515f70 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -78,16 +78,19 @@ "@hyperledger/cactus-core-api": "0.8.0", "axios": "0.21.1", "bl": "5.0.0", + "bn.js": "4.12.0", "express": "4.17.1", - "fabric-ca-client": "2.2.8", - "fabric-common": "2.2.8", - "fabric-network": "2.2.8", + "fabric-ca-client": "2.3.0-snapshot.49", + "fabric-common": "2.3.0-snapshot.49", + "fabric-network": "2.3.0-snapshot.49", "fabric-protos": "2.2.8", "form-data": "4.0.0", "http-status-codes": "2.1.4", + "jsrsasign": "10.4.0", "multer": "1.4.3", "ngo": "2.7.0", "node-ssh": "12.0.0", + "node-vault": "0.9.22", "openapi-types": "9.1.0", "prom-client": "13.2.0", "temp": "0.9.4", @@ -99,7 +102,9 @@ "@hyperledger/cactus-test-tooling": "0.8.0", "@types/express": "4.17.13", "@types/fs-extra": "9.0.12", + "@types/jsrsasign": "8.0.13", "@types/multer": "1.4.7", + "@types/node-vault": "0.9.13", "@types/temp": "0.9.1", "@types/uuid": "8.3.1", "fs-extra": "10.0.0" 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 793826836a..0244a5bed3 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 @@ -31,6 +31,40 @@ ], "components": { "schemas": { + "VaultTransitKey" : { + "type": "object", + "nullable": false, + "required": [ + "keyName", + "token" + ], + "properties": { + "keyName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false, + "description": "label of private key" + }, + "token": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false, + "description": "token for accessing private key" + } + }, + "description": "vault key details for signing fabric message with private key stored with transit engine." + }, + "FabricSigningCredentialType" : { + "type": "string", + "enum": [ + "X.509", + "Vault-X.509" + ], + "nullable": false, + "description": "different type of identity provider for singing fabric messages supported by this package" + }, "FabricSigningCredential": { "type": "object", "required": [ @@ -49,6 +83,14 @@ "minLength": 1, "maxLength": 100, "nullable": false + }, + "type" : { + "$ref" : "#/components/schemas/FabricSigningCredentialType", + "description" : "singing identity type to be used for signing fabric message , by by default default is supported" + }, + "vaultTransitKey" : { + "$ref" : "#/components/schemas/VaultTransitKey", + "properties" : "vault key details , if Vault-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 760ac4c65c..fb4ba76041 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 @@ -1,6 +1,7 @@ import { DefaultEventHandlerOptions } from "fabric-network"; -import { DefaultEventHandlerStrategies, Wallets } from "fabric-network"; +import { DefaultEventHandlerStrategies } from "fabric-network"; import { Gateway } from "fabric-network"; +import { ICryptoKey } from "fabric-common"; import { GatewayOptions as FabricGatewayOptions } from "fabric-network"; import { Checks, LoggerProvider } from "@hyperledger/cactus-common"; import { LogLevelDesc } from "@hyperledger/cactus-common"; @@ -8,7 +9,18 @@ import { PluginRegistry } from "@hyperledger/cactus-core"; import { ConnectionProfile } from "../generated/openapi/typescript-axios/index"; import { GatewayDiscoveryOptions } from "../generated/openapi/typescript-axios/index"; import { GatewayEventHandlerOptions } from "../generated/openapi/typescript-axios/index"; -import { GatewayOptions } from "../generated/openapi/typescript-axios/index"; +import { + GatewayOptions, + FabricSigningCredentialType, +} from "../generated/openapi/typescript-axios/index"; +import { + CertDatastore, + IIdentityData, +} from "../identity/internal/cert-datastore"; +import { + IIdentity, + SecureIdentityProviders, +} from "../identity/identity-provider"; export interface ICreateGatewayContext { readonly logLevel?: LogLevelDesc; @@ -17,6 +29,8 @@ export interface ICreateGatewayContext { readonly defaultDiscoveryOptions: GatewayDiscoveryOptions; readonly defaultEventHandlerOptions: GatewayEventHandlerOptions; readonly gatewayOptions: GatewayOptions; + readonly certStore: CertDatastore; + readonly secureIdentity: SecureIdentityProviders; } export const E_CREATE_GATEWAY_WALLET = @@ -37,26 +51,61 @@ export async function createGateway( const { defaultConnectionProfile } = ctx; const cp = ctx.gatewayOptions.connectionProfile || defaultConnectionProfile; - const wallet = await Wallets.newInMemoryWallet(); - - let identity; + let certData: IIdentityData; if (ctx.gatewayOptions.wallet.json) { log.debug("Parsing wallet from JSON representation..."); - identity = JSON.parse(ctx.gatewayOptions.wallet.json); + certData = JSON.parse(ctx.gatewayOptions.wallet.json); + certData.type = certData.type || FabricSigningCredentialType.X509; } else if (ctx.gatewayOptions.wallet.keychain) { log.debug("Fetching wallet from JSON keychain..."); - const keychain = ctx.pluginRegistry.findOneByKeychainId( + certData = await ctx.certStore.get( ctx.gatewayOptions.wallet.keychain.keychainId, - ); - identity = await keychain.get( ctx.gatewayOptions.wallet.keychain.keychainRef, ); + ctx.gatewayOptions.wallet.keychain.type = + ctx.gatewayOptions.wallet.keychain.type || + FabricSigningCredentialType.X509; + if (certData.type !== ctx.gatewayOptions.wallet.keychain.type) { + throw new Error( + `identity type mismatch, sorted of type = ${certData.type} but provided = ${ctx.gatewayOptions.wallet.keychain.type}`, + ); + } } else { throw new Error(E_CREATE_GATEWAY_WALLET); } - await wallet.put(ctx.gatewayOptions.identity, identity); - log.debug(`Imported identity ${ctx.gatewayOptions.identity} to wallet OK`); + let key: ICryptoKey; + switch (certData.type) { + case FabricSigningCredentialType.VaultX509: + if ( + !ctx.gatewayOptions.wallet.keychain || + !ctx.gatewayOptions.wallet.keychain.vaultTransitKey + ) { + throw new Error( + `require ctx.gatewayOptions.wallet.keychain.vaultTransitKey`, + ); + } + key = ctx.secureIdentity.getVaultKey({ + token: ctx.gatewayOptions.wallet.keychain.vaultTransitKey.token, + keyName: ctx.gatewayOptions.wallet.keychain.vaultTransitKey.keyName, + }); + break; + case FabricSigningCredentialType.X509: + key = ctx.secureIdentity.getDefaultKey({ + private: certData.credentials.privateKey as string, + }); + break; + default: + throw new Error(`UNRECOGNIZED_IDENTITY_TYPE type = ${certData.type}`); + } + const identity: IIdentity = { + type: certData.type, + mspId: certData.mspId, + credentials: { + certificate: certData.credentials.certificate, + key: key, + }, + }; const eventHandlerOptions: DefaultEventHandlerOptions = { commitTimeout: ctx.gatewayOptions.eventHandlerOptions?.commitTimeout || 300, @@ -77,8 +126,8 @@ export async function createGateway( const gatewayOptions: FabricGatewayOptions = { discovery: ctx.gatewayOptions.discovery || ctx.defaultDiscoveryOptions, eventHandlerOptions, - identity: ctx.gatewayOptions.identity, - wallet, + identity: identity, + identityProvider: ctx.secureIdentity, }; log.debug("Instantiating and connecting gateway..."); 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 a29c68fcf2..74678db162 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 @@ -535,7 +535,29 @@ export interface FabricSigningCredential { * @memberof FabricSigningCredential */ keychainRef: string; + /** + * + * @type {FabricSigningCredentialType} + * @memberof FabricSigningCredential + */ + type?: FabricSigningCredentialType; + /** + * + * @type {VaultTransitKey} + * @memberof FabricSigningCredential + */ + vaultTransitKey?: VaultTransitKey; +} +/** + * different type of identity provider for singing fabric messages supported by this package + * @export + * @enum {string} + */ +export enum FabricSigningCredentialType { + X509 = 'X.509', + VaultX509 = 'Vault-X.509' } + /** * Represents a file-system file that has a name and a body which holds the file contents as a Base64 encoded string * @export @@ -791,6 +813,25 @@ export interface SSHExecCommandResponse { */ signal: string | null; } +/** + * vault key details for signing fabric message with private key stored with transit engine. + * @export + * @interface VaultTransitKey + */ +export interface VaultTransitKey { + /** + * label of private key + * @type {string} + * @memberof VaultTransitKey + */ + keyName: string; + /** + * token for accessing private key + * @type {string} + * @memberof VaultTransitKey + */ + token: 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 new file mode 100644 index 0000000000..00dcb2914e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/identity-provider.ts @@ -0,0 +1,134 @@ +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 { + LogLevelDesc, + Logger, + LoggerProvider, +} from "@hyperledger/cactus-common"; +import { Key } from "./internal/key"; +import { InternalCryptoSuite } from "./internal/crypto-suite"; +import { VaultTransitClient } from "./vault-client"; + +export interface IVaultConfig { + endpoint: string; + transitEngineMountPath: string; +} + +export interface ISecureIdentityProvidersOptions { + activatedProviders: FabricSigningCredentialType[]; + logLevel: LogLevelDesc; + + // vault server config + vaultConfig?: IVaultConfig; +} + +export interface IIdentity extends Identity { + type: FabricSigningCredentialType; + credentials: { + certificate: string; + key: ICryptoKey; + }; +} + +export interface VaultKey { + keyName: string; + token: string; +} + +export interface DefaultKey { + // pem encoded private key + private: string; +} + +// SecureIdentityProviders : a entry point class to various secure identity provider +// some of the function are just to support the interface provided by the fabric-sdk-node +export class SecureIdentityProviders implements IdentityProvider { + private readonly log: Logger; + public readonly className = "SecureIdentityProviders"; + private readonly defaultSuite: ICryptoSuite; + constructor(private readonly opts: ISecureIdentityProvidersOptions) { + const fnTag = `${this.className}#constructor`; + this.log = LoggerProvider.getOrCreate({ + level: opts.logLevel || "INFO", + label: this.className, + }); + if ( + opts.activatedProviders.includes(FabricSigningCredentialType.VaultX509) + ) { + if (!opts.vaultConfig) { + throw new Error(`${fnTag} require options.vaultConfig`); + } + Checks.nonBlankString( + opts.vaultConfig.endpoint, + `${fnTag} options.vaultConfig.endpoint`, + ); + Checks.nonBlankString( + opts.vaultConfig.transitEngineMountPath, + `${fnTag} options.vaultConfig.transitEngineMountPath`, + ); + this.log.debug(`${fnTag} Vault-X.509 identity provider activated`); + } + this.defaultSuite = Utils.newCryptoSuite(); + } + + async getUserContext(identity: IIdentity, name: string): Promise { + const fnTag = `${this.className}#getUserContext`; + Checks.truthy(identity, `${fnTag} identity`); + if (!this.opts.activatedProviders.includes(identity.type)) { + throw new Error( + `${fnTag} identity type = ${identity.type} not activated`, + ); + } + Checks.truthy(identity.credentials, `${fnTag} identity.credentials`); + Checks.nonBlankString( + identity.credentials.certificate, + `${fnTag} identity.credentials.certificate`, + ); + Checks.truthy( + identity.credentials.key, + `${fnTag} identity.credentials.key`, + ); + const user = new User(name); + if (identity.type === FabricSigningCredentialType.X509) { + user.setCryptoSuite(this.defaultSuite); + } else { + user.setCryptoSuite(new InternalCryptoSuite()); + } + await user.setEnrollment( + identity.credentials.key, + identity.credentials.certificate, + identity.mspId, + ); + return user; + } + + getVaultKey(key: VaultKey): Key { + return new Key( + key.keyName, + new VaultTransitClient({ + endpoint: this.opts.vaultConfig?.endpoint as string, + mountPath: this.opts.vaultConfig?.transitEngineMountPath as string, + token: key.token, + logLevel: this.opts.logLevel, + }), + ); + } + + getDefaultKey(key: DefaultKey): ICryptoKey { + return this.defaultSuite.createKeyFromRaw(key.private); + } + + // not required things + readonly type = ""; + getCryptoSuite(): ICryptoSuite { + throw new Error("SecureIdentityProviders::getCryptoSuite not required!!"); + } + fromJson(): Identity { + throw new Error("SecureIdentityProviders::fromJson not required!!"); + } + toJson(): IdentityData { + throw new Error("SecureIdentityProviders::toJso : not required!!"); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/cert-datastore.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/cert-datastore.ts new file mode 100644 index 0000000000..b09d9cfb52 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/cert-datastore.ts @@ -0,0 +1,37 @@ +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { FabricSigningCredentialType } from "../../generated/openapi/typescript-axios/api"; + +// IIdentityData : data that will be stored with cert datastore +// with key as client's commonName (from X509 certificate) and value as following field +export interface IIdentityData { + type: FabricSigningCredentialType; + credentials: { + certificate: string; + // if identity type is IdentityProvidersType.Default + privateKey?: string; + }; + mspId: string; +} + +// sweet wrapper for managing client's certificate +// stored within multiple keychain registered to +// plugin registry +export class CertDatastore { + constructor(private readonly pluginRegistry: PluginRegistry) {} + async get(keychainId: string, keychainRef: string): Promise { + const keychain = this.pluginRegistry.findOneByKeychainId(keychainId); + return JSON.parse(await keychain.get(keychainRef)); + } + + async put( + keychainId: string, + keychainRef: string, + iData: IIdentityData, + ): Promise { + const keychain = this.pluginRegistry.findOneByKeychainId(keychainId); + await keychain.set(keychainRef, JSON.stringify(iData)); + } + + // TODO has + // TODO delete +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/client.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/client.ts new file mode 100644 index 0000000000..27bd3f7e1f --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/client.ts @@ -0,0 +1,31 @@ +import { KJUR } from "jsrsasign"; +import { ECCurveType } from "./crypto-util"; + +export interface ISignatureResponse { + sig: Buffer; + crv: ECCurveType; +} + +// class that all the identity provider should implement +export abstract class InternalIdentityClient { + /** + * @description send message digest to the client for it to be signed by the private key stored with the client + * @param keyName , label of the key + * @param digest : messages digest which need to signed (NOTE : digest will already be hashed) + * @returns asn1 encoded signature + */ + abstract sign(keyName: string, digest: Buffer): Promise; + + /** + * @description get the the public key from the client + * @param keyName for which public key should be returned + * @returns ECDSA key only p256 and p384 curve are supported + */ + abstract getPub(keyName: string): Promise; + + /** + * @description will rotate a given key + * @param keyName label of key that need to be rotated + */ + abstract rotateKey(keyName: string): Promise; +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-suite.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-suite.ts new file mode 100644 index 0000000000..40f7e4159c --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-suite.ts @@ -0,0 +1,45 @@ +import { ICryptoSuite, ICryptoKey } from "fabric-common"; +import { createHash } from "crypto"; +import { Key } from "./key"; +import { Utils } from "fabric-common"; + +// InternalCryptoSuite : a class which will be implemented by identity provider +// some of the function are just to support the interface provided by the fabric-sdk-node +export class InternalCryptoSuite implements ICryptoSuite { + createKeyFromRaw(pem: string): ICryptoKey { + return Utils.newCryptoSuite().createKeyFromRaw(pem); + } + decrypt(): Buffer { + throw new Error("InternalCryptoSuite::decrypt : not required!!"); + } + deriveKey(): ICryptoKey { + throw new Error("InternalCryptoSuite::deriveKey : not required!!"); + } + encrypt(): Buffer { + throw new Error("InternalCryptoSuite::encrypt : not required!!"); + } + getKey(): Promise { + throw new Error("InternalCryptoSuite::getKey : not required!!"); + } + getKeySize(): number { + throw new Error("InternalCryptoSuite::getKeySize : not required!!"); + } + generateKey(): Promise { + throw new Error("InternalCryptoSuite::generateKey : not required!!"); + } + hash(msg: string): string { + return createHash("sha256").update(msg).digest("hex"); + } + importKey(): ICryptoKey | Promise { + throw new Error("InternalCryptoSuite::importKey : not required!!"); + } + setCryptoKeyStore(): void { + throw new Error("InternalCryptoSuite::setCryptoKeyStore : not required!!"); + } + async sign(key: Key, digest: Buffer): Promise { + return await key.sign(digest); + } + verify(): boolean { + throw new Error("InternalCryptoSuite::verify : not required!!"); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-util.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-util.ts new file mode 100644 index 0000000000..0942be1478 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/crypto-util.ts @@ -0,0 +1,87 @@ +import { createHash } from "crypto"; +import { KJUR } from "jsrsasign"; +import BN from "bn.js"; + +const csrNamespace = KJUR.asn1.csr as any; + +// hex encoded prime order of NIST P-256 curve +// more information at https://safecurves.cr.yp.to/base.html +const ecdsa = new KJUR.crypto.ECDSA({ curve: "NIST P-256" }); +const p256N = new BN(((ecdsa as any).ecparams.n as BigInteger).toString(), 10); +// hex encoded prime order of NIST P-384 curve +ecdsa.setNamedCurve("NIST P-384"); +const p384N = new BN(((ecdsa as any).ecparams.n as BigInteger).toString(), 10); + +export enum ECCurveType { + P256 = "p256", + P384 = "p384", +} + +// class with all static function +// provide crypto util to identity providers +export class CryptoUtil { + public static readonly className = "CryptoUtil"; + // convert asn1 encoded signature to a fabric understandable signature format + // more info at https://github.com/hyperledger/fabric-sdk-node/blob/b562ae4d7b8c690cd008c98ff24dfd3fb78ade81/fabric-common/lib/impl/bccsp_pkcs11.js#L39 + static encodeASN1Sig(sig: Buffer, curve: ECCurveType): Buffer { + const fnTag = `${CryptoUtil.className}#encodeASN1Sig`; + const pSig = (KJUR.crypto.ECDSA as any).parseSigHexInHexRS( + sig.toString("hex"), + ) as { r: string; s: string }; + const r = new BN(pSig.r, "hex"); + let s = new BN(pSig.s, "hex"); + let crv: BN; + switch (curve) { + case ECCurveType.P256: + crv = p256N; + break; + case ECCurveType.P384: + crv = p384N; + break; + default: + throw new Error(`${fnTag} invalid ec curve type`); + } + const halfOrder = crv.shrn(1); + if (s.cmp(halfOrder) === 1) { + const bigNum = crv as BN; + s = bigNum.sub(s); + } + const encodedSig = KJUR.crypto.ECDSA.hexRSSigToASN1Sig( + r.toString("hex"), + s.toString("hex"), + ); + return Buffer.from(encodedSig, "hex"); + } + + // create a csr information using public key and commonName + // return s csr object + static createCSR( + pub: KJUR.crypto.ECDSA, + commonName: string, + ): KJUR.asn1.csr.CertificationRequest { + return new csrNamespace.CertificationRequest({ + subject: { str: "/CN=" + commonName }, + sbjpubkey: pub, + sigalg: "SHA256withECDSA", + }); + } + + // return csr digest to e signed by a private key + // signature should be a asn1 der encoded + static getCSRDigest(csr: KJUR.asn1.csr.CertificationRequest): Buffer { + const csrInfo = new csrNamespace.CertificationRequestInfo( + (csr as any).params, + ); + return createHash("sha256").update(csrInfo.getEncodedHex(), "hex").digest(); + } + + // generate a pem encoded csr + static getPemCSR( + _csr: KJUR.asn1.csr.CertificationRequest, + signature: Buffer, + ): string { + const csr = _csr as any; + csr.params.sighex = signature.toString("hex"); + return csr.getPEM(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/key.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/key.ts new file mode 100644 index 0000000000..c581c022e7 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/internal/key.ts @@ -0,0 +1,54 @@ +import { ICryptoKey } from "fabric-common"; +import { InternalIdentityClient } from "./client"; +import { CryptoUtil } from "./crypto-util"; + +// internal class used by cryptoSuite, this is just to support interface provided by +// fabric-sdk-node +export class Key implements ICryptoKey { + constructor( + private readonly keyName: string, + private readonly client: InternalIdentityClient, + ) {} + async sign(digest: Buffer): Promise { + const { sig, crv } = await this.client.sign(this.keyName, digest); + return CryptoUtil.encodeASN1Sig(sig, crv); + } + + /** + * @description generate a csr + * @param commonName + * @returns pem encoded csr string + */ + async generateCSR(commonName: string): Promise { + const pub = await this.client.getPub(this.keyName); + const csr = CryptoUtil.createCSR(pub, commonName); + const digest = CryptoUtil.getCSRDigest(csr); + const { sig } = await this.client.sign(this.keyName, digest); + return CryptoUtil.getPemCSR(csr, sig); + } + + /** + * @description will rotate the key + */ + async rotate(): Promise { + await this.client.rotateKey(this.keyName); + } + getSKI(): string { + throw new Error("Key::getSKI not-required"); + } + getHandle(): string { + throw new Error("Key::getHandle not-required"); + } + isSymmetric(): boolean { + throw new Error("Key::isSymmetric not-required"); + } + isPrivate(): boolean { + throw new Error("Key::isPrivate not-required"); + } + getPublicKey(): ICryptoKey { + throw new Error("Key::getPublicKey not-required"); + } + toBytes(): string { + throw new Error("Key::toBytes not-required"); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/vault-client.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/vault-client.ts new file mode 100644 index 0000000000..a720995b16 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/identity/vault-client.ts @@ -0,0 +1,117 @@ +import { + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import Vault, { client } from "node-vault"; +import { KJUR, KEYUTIL } from "jsrsasign"; +import { InternalIdentityClient, ISignatureResponse } from "./internal/client"; +import { ECCurveType } from "./internal/crypto-util"; + +export interface IVaultTransitClientOptions { + // full url of vault server + // eg : http://localhost:8200 + endpoint: string; + + // mountPath of transit secret engine + // eg : /transit + mountPath: string; + + // token of the client + token: string; + + logLevel?: LogLevelDesc; +} + +export class VaultTransitClient implements InternalIdentityClient { + public readonly className = "VaultTransitClient"; + private readonly log: Logger; + private readonly backend: client; + constructor(opts: IVaultTransitClientOptions) { + this.log = LoggerProvider.getOrCreate({ + label: "VaultTransitClient", + level: opts.logLevel || "INFO", + }); + this.backend = Vault({ + endpoint: opts.endpoint, + apiVersion: "v1", + token: opts.token, + pathPrefix: opts.mountPath, + }); + } + + /** + * @description send message digest to be signed by private key stored on vault + * @param digest : messages digest which need to signed + * @param preHashed : is digest already hashed + * @returns asn1 encoded signature + */ + async sign(keyName: string, digest: Buffer): Promise { + const fnTag = `${this.className}#sign`; + this.log.debug( + `${fnTag} sign with key = ${keyName} , digestSize = ${digest.length}`, + ); + const pub = await this.getPub(keyName); + let crv = ECCurveType.P256; + if ((pub as any).curveName === "secp384r1") { + crv = ECCurveType.P384; + } + const resp = await this.backend.write("sign/" + keyName, { + input: digest.toString("base64"), + prehashed: true, + marshaling_algorithm: "asn1", + }); + this.log.debug(`${fnTag} got response from vault : %o`, resp.data); + if (resp?.data?.signature) { + const base64Sig = (resp.data.signature as string).split(":")[2]; + return { + sig: Buffer.from(base64Sig, "base64"), + crv: crv, + }; + } + throw new Error(`invalid response from vault ${JSON.stringify(resp)}`); + } + + /** + * @description return public key of latest version + * @param keyName for which public key should be returned + * @returns pem encoded public key + */ + async getPub(keyName: string): Promise { + const fnTag = `${this.className}#getPub`; + this.log.debug(`${fnTag} keyName = ${keyName}`); + try { + const resp = await this.backend.read("keys/" + keyName); + this.log.debug(`${fnTag} Response from Vault: %o`, JSON.stringify(resp)); + if (resp?.data?.latest_version && resp?.data?.keys) { + if (!["ecdsa-p256", "ecdsa-p384"].includes(resp.data.type)) { + throw new Error(`${fnTag} key = ${keyName} has invalid key type`); + } + // resp.data.keys has array of all the version of the key + // latest version is used for signing + return KEYUTIL.getKey( + resp.data.keys[resp.data.latest_version].public_key, + ) as KJUR.crypto.ECDSA; + } + throw new Error( + `${fnTag} invalid response from vault ${JSON.stringify(resp)}`, + ); + } catch (error) { + if ((error as any).response?.statusCode === 404) { + throw new Error(`${fnTag} keyName = ${keyName} not found`); + } + throw error; + } + } + + /** + * @description will rotate a given key + * @param keyName label of key that need to be rotated + */ + async rotateKey(keyName: string): Promise { + const fnTag = `${this.className}#rotateKey`; + this.log.debug(`${fnTag} rotate the kew ${keyName}`); + await this.backend.write("keys/" + keyName + "/rotate", {}); + this.log.debug(`${fnTag} key = ${keyName} successfully rotated`); + } +} 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 e02c205ca5..29b0a69d79 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 @@ -72,6 +72,7 @@ import { ChainCodeLifeCycleCommandResponses, FabricSigningCredential, DefaultEventHandlerStrategy, + FabricSigningCredentialType, } from "./generated/openapi/typescript-axios/index"; import { @@ -87,10 +88,21 @@ import { IDeployContractEndpointV1Options, } from "./deploy-contract/deploy-contract-endpoint-v1"; import { sourceLangToRuntimeLang } from "./peer/source-lang-to-runtime-lang"; -import FabricCAServices from "fabric-ca-client"; +import FabricCAServices, { + IEnrollmentRequest, + IRegisterRequest, +} from "fabric-ca-client"; import { createGateway } from "./common/create-gateway"; -import { Endorser } from "fabric-common"; - +import { Endorser, ICryptoKey } from "fabric-common"; +import { + IVaultConfig, + SecureIdentityProviders, + IIdentity, +} from "./identity/identity-provider"; +import { + CertDatastore, + IIdentityData, +} from "./identity/internal/cert-datastore"; /** * Constant value holding the default $GOPATH in the Fabric CLI container as * observed on fabric deployments that are produced by the official examples @@ -118,6 +130,8 @@ export interface IPluginLedgerConnectorFabricOptions prometheusExporter?: PrometheusExporter; discoveryOptions?: GatewayDiscoveryOptions; eventHandlerOptions?: GatewayEventHandlerOptions; + supportedIdentity?: FabricSigningCredentialType[]; + vaultConfig?: IVaultConfig; } export class PluginLedgerConnectorFabric @@ -139,6 +153,8 @@ export class PluginLedgerConnectorFabric private readonly cliContainerGoPath: string; public prometheusExporter: PrometheusExporter; private endpoints: IWebServiceEndpoint[] | undefined; + private readonly secureIdentity: SecureIdentityProviders; + private readonly certStore: CertDatastore; public get className(): string { return PluginLedgerConnectorFabric.CLASS_NAME; @@ -175,6 +191,15 @@ export class PluginLedgerConnectorFabric this.log = LoggerProvider.getOrCreate({ level, label }); this.instanceId = opts.instanceId; this.prometheusExporter.startMetricsCollection(); + // default is supported if supportedIdentity is empty + this.secureIdentity = new SecureIdentityProviders({ + activatedProviders: opts.supportedIdentity || [ + FabricSigningCredentialType.X509, + ], + logLevel: opts.logLevel || "INFO", + vaultConfig: opts.vaultConfig, + }); + this.certStore = new CertDatastore(opts.pluginRegistry); } public async shutdown(): Promise { @@ -823,6 +848,8 @@ export class PluginLedgerConnectorFabric strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, }, gatewayOptions: req.gatewayOptions, + secureIdentity: this.secureIdentity, + certStore: this.certStore, }); } else { return this.createGatewayLegacy(req.signingCredential); @@ -834,27 +861,44 @@ export class PluginLedgerConnectorFabric ): Promise { const { connectionProfile, eventHandlerOptions: eho } = this.opts; - const wallet = await Wallets.newInMemoryWallet(); + const iType = signingCredential.type || FabricSigningCredentialType.X509; - const keychain = this.opts.pluginRegistry.findOneByKeychainId( - signingCredential.keychainId, - ); - this.log.debug( - "transact() obtained keychain by ID=%o OK", + const certData = await this.certStore.get( signingCredential.keychainId, - ); - - const fabricX509IdentityJson = await keychain.get( signingCredential.keychainRef, ); - this.log.debug( - "transact() obtained keychain entry Key=%o OK", - signingCredential.keychainRef, - ); - const identity = JSON.parse(fabricX509IdentityJson); - - await wallet.put(signingCredential.keychainRef, identity); - this.log.debug("transact() imported identity to in-memory wallet OK"); + if (iType !== certData.type) { + throw new Error( + `identity type mismatch, sorted of type = ${certData.type} but provided = ${iType}`, + ); + } + let key: ICryptoKey; + switch (iType) { + case FabricSigningCredentialType.VaultX509: + if (!signingCredential.vaultTransitKey) { + throw new Error(`require signingCredential.vaultTransitKey`); + } + key = this.secureIdentity.getVaultKey({ + token: signingCredential.vaultTransitKey.token, + keyName: signingCredential.vaultTransitKey.keyName, + }); + break; + case FabricSigningCredentialType.X509: + key = this.secureIdentity.getDefaultKey({ + private: certData.credentials.privateKey as string, + }); + break; + default: + throw new Error(`UNRECOGNIZED_IDENTITY_TYPE type = ${iType}`); + } + const identity: IIdentity = { + type: iType, + mspId: certData.mspId, + credentials: { + certificate: certData.credentials.certificate, + key: key, + }, + }; const eventHandlerOptions: DefaultEventHandlerOptions = { commitTimeout: this.opts.eventHandlerOptions?.commitTimeout || 300, @@ -868,8 +912,8 @@ export class PluginLedgerConnectorFabric const gatewayOptions: GatewayOptions = { discovery: this.opts.discoveryOptions, eventHandlerOptions, - identity: signingCredential.keychainRef, - wallet, + identity: identity, + identityProvider: this.secureIdentity, }; this.log.debug(`discovery=%o`, gatewayOptions.discovery); @@ -904,6 +948,7 @@ export class PluginLedgerConnectorFabric try { const gateway = await this.createGateway(req); + // const gateway = await this.createGatewayLegacy(req.signingCredential); const network = await gateway.getNetwork(channelName); // const channel = network.getChannel(); // const endorsers = channel.getEndorsers(); @@ -997,6 +1042,7 @@ export class PluginLedgerConnectorFabric functionOutput: outUtf8, success, }; + gateway.disconnect(); this.log.debug(`transact() response: %o`, res); this.prometheusExporter.addCurrentTransaction(); @@ -1067,4 +1113,175 @@ export class PluginLedgerConnectorFabric throw new Error(`${fnTag} Exception: ${ex?.message}`); } } + /** + * @description enroll a client and store the enrolled certificate inside keychain + * @param identity details about client's key + * @param request , enroll request for fabric-ca-server + */ + public async enroll( + identity: FabricSigningCredential, + request: { + enrollmentID: string; + enrollmentSecret: string; + caId: string; + mspId: string; + }, + ): Promise { + const fnTag = `${this.className}#enroll`; + const iType = identity.type || FabricSigningCredentialType.X509; + this.log.debug( + `${fnTag} enroll identity of type = ${iType} with ca = ${request.caId}`, + ); + Checks.nonBlankString(identity.keychainId, `${fnTag} identity.keychainId`); + Checks.nonBlankString( + identity.keychainRef, + `${fnTag} identity.keychainRef`, + ); + Checks.nonBlankString(request.mspId, `${fnTag} request.mspId`); + const ca = await this.createCaClient(request.caId); + const enrollmentRequest: IEnrollmentRequest = { + enrollmentID: request.enrollmentID, + enrollmentSecret: request.enrollmentSecret, + }; + switch (iType) { + case FabricSigningCredentialType.VaultX509: + if (!identity.vaultTransitKey) { + throw new Error(`${fnTag} require identity.vaultTransitKey`); + } + const key = this.secureIdentity.getVaultKey({ + token: identity.vaultTransitKey.token, + keyName: identity.vaultTransitKey.keyName, + }); + enrollmentRequest.csr = await key.generateCSR(request.enrollmentID); + break; + } + const resp = await ca.enroll(enrollmentRequest); + const certData: IIdentityData = { + type: iType, + mspId: request.mspId, + credentials: { + certificate: resp.certificate, + }, + }; + if (resp.key) { + certData.credentials.privateKey = resp.key.toBytes(); + } + await this.certStore.put( + identity.keychainId, + identity.keychainRef, + certData, + ); + } + + public async register( + registrar: FabricSigningCredential, + request: IRegisterRequest, + caId: string, + ): Promise { + const fnTag = `${this.className}#register`; + const iType = registrar.type || FabricSigningCredentialType.X509; + this.log.debug( + `${fnTag} register client using registrar identity of type = ${iType}`, + ); + Checks.nonBlankString( + registrar.keychainId, + `${fnTag} registrar.keychainId`, + ); + Checks.nonBlankString( + registrar.keychainRef, + `${fnTag} registrar.keychainRef`, + ); + const certData = await this.certStore.get( + registrar.keychainId, + registrar.keychainRef, + ); + if (certData.type != iType) { + throw new Error( + `${fnTag} identity type mismatch, stored ${certData.type} but provided ${iType}`, + ); + } + let key: ICryptoKey; + switch (iType) { + case FabricSigningCredentialType.X509: + key = this.secureIdentity.getDefaultKey({ + private: certData.credentials.privateKey as string, + }); + break; + case FabricSigningCredentialType.VaultX509: + if (!registrar.vaultTransitKey) { + throw new Error(`${fnTag} require registrar.vaultTransitKey`); + } + key = this.secureIdentity.getVaultKey({ + token: registrar.vaultTransitKey.token, + keyName: registrar.vaultTransitKey.keyName, + }); + break; + default: + throw new Error(`${fnTag} UNRECOGNIZED_IDENTITY_TYPE type = ${iType}`); + } + const user = await this.secureIdentity.getUserContext( + { + type: iType, + credentials: { + certificate: certData.credentials.certificate, + key: key, + }, + mspId: certData.mspId, + }, + "registrar", + ); + + const ca = await this.createCaClient(caId); + return await ca.register(request, user); + } + + /** + * @description re-enroll a client with new private key + * @param identity + */ + public async rotateKey( + identity: FabricSigningCredential, + request: { + enrollmentID: string; + enrollmentSecret: string; + caId: string; + }, + ): Promise { + const fnTag = `${this.className}#rotateKey`; + const iType = identity.type || FabricSigningCredentialType.X509; + this.log.debug( + `${fnTag} identity of type = ${iType} with ca = ${request.caId}`, + ); + this.log.debug( + `${fnTag} enroll identity of type = ${iType} with ca = ${request.caId}`, + ); + Checks.nonBlankString(identity.keychainId, `${fnTag} identity.keychainId`); + Checks.nonBlankString( + identity.keychainRef, + `${fnTag} identity.keychainRef`, + ); + const certData = await this.certStore.get( + identity.keychainId, + identity.keychainRef, + ); + switch (iType) { + case FabricSigningCredentialType.VaultX509: + if (!identity.vaultTransitKey) { + throw new Error(`${fnTag} require identity.vaultTransitKey)`); + } + const key = this.secureIdentity.getVaultKey({ + keyName: identity.vaultTransitKey.keyName, + token: identity.vaultTransitKey.token, + }); + await key.rotate(); + break; + } + identity.type = iType; + await this.enroll(identity, { + enrollmentID: request.enrollmentID, + enrollmentSecret: request.enrollmentSecret, + caId: request.caId, + mspId: certData.mspId, + }); + } } 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 18f8810f52..e15a92a081 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 @@ -22,3 +22,6 @@ export { ICompilationOptions, ICompilationResult, } from "./chain-code-compiler"; + +export { IVaultConfig } 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-identities.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts new file mode 100644 index 0000000000..d6d894873a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts @@ -0,0 +1,371 @@ +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, + IVaultConfig, + PluginLedgerConnectorFabric, + IIdentityData, + FabricContractInvocationType, +} from "../../../../main/typescript/public-api"; +import { DiscoveryOptions } from "fabric-network"; + +const logLevel: LogLevelDesc = "TRACE"; +import { + Containers, + VaultTestServer, + K_DEFAULT_VAULT_HTTP_PORT, + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; +import { v4 as internalIpV4 } from "internal-ip"; +import axios from "axios"; + +// test scenario +// - enroll registrar (both using default identity and vault(p256) identity) +// - register 2 client (using registrar identity) +// - enroll 1st client using default identity +// - enroll 2nd client using vault 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-identities", 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-04-20-nodejs", + envVars: new Map([["FABRIC_VERSION", "2.2.0"]]), + logLevel, + }); + + const tearDown = async () => { + await ledger.stop(); + await ledger.destroy(); + await pruneDockerAllIfGithubAction({ logLevel }); + }; + + test.onFinish(tearDown); + await ledger.start(); + + const connectionProfile = await ledger.getConnectionProfileOrg1(); + t.ok(connectionProfile, "getConnectionProfileOrg1() out truthy OK"); + + const registrarKey = "registrar"; + const client1Key = "client-default-"; + const client2Key = "client-vault"; + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + ///// + const vaultTestContainer = new VaultTestServer({}); + await vaultTestContainer.start(); + + const ci = await Containers.getById(vaultTestContainer.containerId); + const vaultIpAddr = await internalIpV4(); + const hostPort = await Containers.getPublicPort( + K_DEFAULT_VAULT_HTTP_PORT, + ci, + ); + const vaultHost = `http://${vaultIpAddr}:${hostPort}`; + ///// + const vaultConfig: IVaultConfig = { + endpoint: vaultHost, + transitEngineMountPath: "/transit", + }; + const testToken = "myroot"; + { + // mount engine and create some test keys + const vaultHTTPClient = axios.create({ + baseURL: vaultConfig.endpoint + "/v1", + headers: { + "X-Vault-Token": testToken, + }, + }); + await vaultHTTPClient.post( + "/sys/mounts" + vaultConfig.transitEngineMountPath, + { type: "transit" }, + ); + await vaultHTTPClient.post( + vaultConfig.transitEngineMountPath + "/keys/" + registrarKey, + { + type: "ecdsa-p256", + }, + ); + await vaultHTTPClient.post( + vaultConfig.transitEngineMountPath + "/keys/" + client2Key, + { + type: "ecdsa-p384", + }, + ); + } + test.onFinish(async () => { + await vaultTestContainer.stop(); + await vaultTestContainer.destroy(); + }); + /// + 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.VaultX509, + FabricSigningCredentialType.X509, + ]; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: uuidv4(), + pluginRegistry, + sshConfig: {}, + cliContainerEnv: {}, + peerBinary: "not-required", + logLevel, + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + supportedIdentity, + vaultConfig: vaultConfig, + }; + const plugin = new PluginLedgerConnectorFabric(pluginOptions); + t.test("with-vaultKey", async (t: Test) => { + { + // enroll registrar using default identity + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: registrarKey + "-x.509", + type: FabricSigningCredentialType.X509, + }, + { + enrollmentID: "admin", + enrollmentSecret: "adminpw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); + + const rawCert = await keychainPlugin.get(registrarKey + "-x.509"); + t.ok(rawCert); + const certData = JSON.parse(rawCert) as IIdentityData; + t.equal(certData.type, FabricSigningCredentialType.X509); + t.ok(certData.credentials.privateKey); + } + { + // enroll registrar using vault identity + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: registrarKey + "-vault", + type: FabricSigningCredentialType.VaultX509, + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + { + enrollmentID: "admin", + enrollmentSecret: "adminpw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); + + const rawCert = await keychainPlugin.get(registrarKey + "-vault"); + t.ok(rawCert); + const certData = JSON.parse(rawCert) as IIdentityData; + t.equal(certData.type, FabricSigningCredentialType.VaultX509); + t.notok(certData.credentials.privateKey); + } + { + // register a client1 using registrar's default x509 identity + const secret = await plugin.register( + { + keychainId: keychainId, + keychainRef: registrarKey + "-x.509", + }, + { + enrollmentID: client1Key, + enrollmentSecret: "pw", + affiliation: "org1.department1", + }, + "ca.org1.example.com", + ); + t.equal(secret, "pw"); + } + { + // register a client using registrar's vault identity + const secret = await plugin.register( + { + keychainId: keychainId, + keychainRef: registrarKey + "-vault", + type: FabricSigningCredentialType.VaultX509, + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + { + enrollmentID: client2Key, + enrollmentSecret: "pw", + affiliation: "org1.department1", + }, + "ca.org1.example.com", + ); + t.equal(secret, "pw"); + } + + { + // enroll client client1 registered above + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: client1Key, + }, + { + enrollmentID: client1Key, + enrollmentSecret: "pw", + mspId: "Org1MSP", + caId: "ca.org1.example.com", + }, + ); + + const rawCert = await keychainPlugin.get(client1Key); + t.ok(rawCert); + const certData = JSON.parse(rawCert) as IIdentityData; + t.equal(certData.type, FabricSigningCredentialType.X509); + t.ok(certData.credentials.privateKey); + } + { + // enroll client2 using vault identity + await plugin.enroll( + { + keychainId: keychainId, + keychainRef: client2Key, + type: FabricSigningCredentialType.VaultX509, + vaultTransitKey: { + token: testToken, + keyName: client2Key, + }, + }, + { + 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.VaultX509, "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: client1Key, + }, + 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.VaultX509, + vaultTransitKey: { + token: testToken, + keyName: client2Key, + }, + }, + channelName: "mychannel", + contractName, + invocationType: FabricContractInvocationType.Send, + methodName: "TransferAsset", + params: ["asset1", "client2"], + }); + t.true(resp.success, "TransferAsset asset1 client2 success true OK"); + } + { + // make query ReadAsset using a registrar client + const resp = await plugin.transact({ + signingCredential: { + keychainId: keychainId, + keychainRef: registrarKey + "-vault", + type: FabricSigningCredentialType.VaultX509, + vaultTransitKey: { + token: testToken, + keyName: registrarKey, + }, + }, + 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 new file mode 100644 index 0000000000..3d1b5513f6 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/identity-client.test.ts @@ -0,0 +1,203 @@ +import axios from "axios"; +import { v4 as internalIpV4 } from "internal-ip"; +import { + Containers, + VaultTestServer, + K_DEFAULT_VAULT_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 { LogLevelDesc } from "@hyperledger/cactus-common"; +import { createHash } from "crypto"; +import { ECCurveType } from "../../../main/typescript/identity/internal/crypto-util"; +import { KJUR } from "jsrsasign"; + +const logLevel: LogLevelDesc = "TRACE"; +// a generic test suite for testing all the identity clients +// supported by this package +test("identity-clients", async (t: Test) => { + const testClients: Map = new Map(); + const testECP256 = "test-ec-p256"; + const testECP384 = "test-ec-p384"; + const testNotFoundKey = "keyNotFound"; + { + // setup vault client + const vaultTestContainer = new VaultTestServer({}); + await vaultTestContainer.start(); + + const ci = await Containers.getById(vaultTestContainer.containerId); + const vaultIpAddr = await internalIpV4(); + const hostPort = await Containers.getPublicPort( + K_DEFAULT_VAULT_HTTP_PORT, + ci, + ); + const vaultHost = `http://${vaultIpAddr}:${hostPort}`; + test.onFinish(async () => { + await vaultTestContainer.stop(); + await vaultTestContainer.destroy(); + }); + const mountPath = "/transit"; + const testToken = "myroot"; + // mount transit secret engine + await axios.post( + vaultHost + "/v1/sys/mounts" + mountPath, + { + type: "transit", + }, + { + headers: { + "X-Vault-Token": testToken, + }, + }, + ); + await axios.post( + vaultHost + "/v1" + mountPath + "/keys/" + testECP256, + { + type: "ecdsa-p256", + }, + { + headers: { + "X-Vault-Token": testToken, + }, + }, + ); + await axios.post( + vaultHost + "/v1" + mountPath + "/keys/" + testECP384, + { + type: "ecdsa-p384", + }, + { + headers: { + "X-Vault-Token": testToken, + }, + }, + ); + testClients.set( + "vault-client", + new VaultTransitClient({ + endpoint: vaultHost, + mountPath: mountPath, + token: testToken, + logLevel: 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) => { + { + const { sig, crv } = await client.sign(testECP256, hashDigest); + t.equal(crv, ECCurveType.P256); + t.ok(sig); + { + // asn1 encoding check + const pSig = (KJUR.crypto.ECDSA as any).parseSigHexInHexRS( + sig.toString("hex"), + ) as { r: string; s: string }; + const re = /[0-9A-Fa-f]{6}/g; + t.true(re.test(pSig.r)); + t.true(re.test(pSig.s)); + } + + { + // signature verification + const pub = await client.getPub(testECP256); + const verify = new KJUR.crypto.Signature({ alg: "SHA256withECDSA" }); + verify.init(pub); + verify.updateHex(digest.toString("hex")); + t.true(verify.verify(sig.toString("hex"))); + } + } + { + const { sig, crv } = await client.sign(testECP384, hashDigest); + t.equal(crv, ECCurveType.P384); + t.ok(sig); + { + // asn1 encoding check + const pSig = (KJUR.crypto.ECDSA as any).parseSigHexInHexRS( + sig.toString("hex"), + ) as { r: string; s: string }; + const re = /[0-9A-Fa-f]{6}/g; + t.true(re.test(pSig.r)); + t.true(re.test(pSig.s)); + } + + { + // signature verification + const pub = await client.getPub(testECP384); + const verify = new KJUR.crypto.Signature({ alg: "SHA256withECDSA" }); + verify.init(pub); + verify.updateHex(digest.toString("hex")); + t.true(verify.verify(sig.toString("hex"))); + } + } + t.end(); + }); + t.test(`${clientName}::getPub`, async (t: Test) => { + { + const pub = await client.getPub(testECP256); + t.ok(pub); + t.equal((pub as any).curveName, "secp256r1"); + } + { + const pub = await client.getPub(testECP384); + t.ok(pub); + t.equal((pub as any).curveName, "secp384r1"); + } + { + try { + await client.getPub(testNotFoundKey); + t.fail("Should not get here"); + } catch (error) { + t.true( + (error as Error).message.includes( + `keyName = ${testNotFoundKey} not found`, + ), + ); + } + } + t.end(); + }); + t.test(`${clientName}::rotateKey`, async (t: Test) => { + { + const pubOld = await client.getPub(testECP256); + await client.rotateKey(testECP256); + const pubNew = await client.getPub(testECP256); + { + // public key should be different + t.notEqual(pubNew.getPublicKeyXYHex(), pubOld.getPublicKeyXYHex()); + } + { + // signature should be made using new key + const { sig } = await client.sign(testECP256, hashDigest); + const verify = new KJUR.crypto.Signature({ alg: "SHA256withECDSA" }); + verify.init(pubOld); + verify.updateHex(digest.toString("hex")); + t.false(verify.verify(sig.toString("hex"))); + } + } + { + const pubOld = await client.getPub(testECP384); + await client.rotateKey(testECP384); + const pubNew = await client.getPub(testECP384); + { + // public key should be different + t.notEqual(pubNew.getPublicKeyXYHex(), pubOld.getPublicKeyXYHex()); + } + { + // signature should be made using new key + const { sig } = await client.sign(testECP384, hashDigest); + const verify = new KJUR.crypto.Signature({ alg: "SHA256withECDSA" }); + verify.init(pubOld); + verify.updateHex(digest.toString("hex")); + t.false(verify.verify(sig.toString("hex"))); + } + } + t.end(); + }); + } + t.end(); +}); 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 new file mode 100644 index 0000000000..ab4ff9338a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts @@ -0,0 +1,81 @@ +import { KEYUTIL, KJUR } from "jsrsasign"; +import test, { Test } from "tape"; +import { + CryptoUtil, + ECCurveType, +} from "../../../main/typescript/identity/internal/crypto-util"; +const KJ = KJUR as any; +test("cryptoUtil", (t: Test) => { + t.test("encodeASN1Sig", (t: Test) => { + { + const asn1Sig = + "MEYCIQDb+euisbUGQCpisQh9xEKof8zVNorerfhMHv3kWmuCfQIhAI8A3f21hAHga0WK6lS6cD/ZUtWVy/xsYZGGaldM7Tl3"; + const derSig = CryptoUtil.encodeASN1Sig( + Buffer.from(asn1Sig, "base64"), + ECCurveType.P256, + ); + const want = + "3045022100dbf9eba2b1b506402a62b1087dc442a87fccd5368adeadf84c1efde45a6b827d022070ff22014a7bfe2094ba7515ab458fbfe3942517db1b32236233606baf75ebda"; + t.equal(derSig.toString("hex"), want); + } + + { + const asn1Sig = + "MGYCMQDFEvlhmQlTj7YGTjnZwERmj+/0IA2rDdb7F5iE1QLGQUUlHn353mizpFeXAcjWGH4CMQDZ1td1ISnjAPkUlohwjiJAShtaFITLG1NEr0G29Hgglt0mfvgJ0k2DXXy+mOyn57o="; + const derSig = CryptoUtil.encodeASN1Sig( + Buffer.from(asn1Sig, "base64"), + ECCurveType.P384, + ); + const want = + "3065023100c512f9619909538fb6064e39d9c044668feff4200dab0dd6fb179884d502c64145251e7df9de68b3a4579701c8d6187e02302629288aded61cff06eb69778f71ddbfb5e4a5eb7b34e4ac82b40bcaffbf0d487af38eba3ede59f78f6f5ad1e01d41b9"; + t.equal(derSig.toString("hex"), want); + } + { + const asn1Sig = + "MGYCMQDFEvlhmQlTj7YGTjnZwERmj+/0IA2rDdb7F5iE1QLGQUUlHn353mizpFeXAcjWGH4CMQDZ1td1ISnjAPkUlohwjiJAShtaFITLG1NEr0G29Hgglt0mfvgJ0k2DXXy+mOyn57o="; + try { + CryptoUtil.encodeASN1Sig( + Buffer.from(asn1Sig, "base64"), + "invalidCrv" as ECCurveType, + ); + } catch (error) { + t.equal( + "CryptoUtil#encodeASN1Sig invalid ec curve type", + (error as Error).message, + ); + } + } + t.end(); + }); + + t.test("CSR", (t: Test) => { + const pem = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEziQUVVrao4nXmhe3jaMdsxAyszHY +GfNTZZQn1F9PQCxOequ4XS4XFmng3MD8jkP58Sak/6QaXYvqAEB6pBT/gA== +-----END PUBLIC KEY----- +`; + const csr = CryptoUtil.createCSR( + KEYUTIL.getKey(pem) as KJUR.crypto.ECDSA, + "Cactus", + ); + t.ok(csr); + const csrDigest = CryptoUtil.getCSRDigest(csr); + t.equal( + csrDigest.toString("base64"), + "S5E8XQhxbltjJLE2n3krEOC5cgmENCKtvUrj3AX4StY=", + ); + const signature = Buffer.from( + "MEYCIQDzWXNQkzf4DO2Ds7MJ4RdIdQfIGbsRpK5iQAmRWyQvpAIhAKVHJL2yFIQba/S09XccNCEZhfZW3XvFqY54rz4ZIjpV", + "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(); + }); + t.end(); +}); diff --git a/packages/cactus-test-tooling/src/main/typescript/common/containers.ts b/packages/cactus-test-tooling/src/main/typescript/common/containers.ts index 888fe3f4b2..140a2f9e5c 100644 --- a/packages/cactus-test-tooling/src/main/typescript/common/containers.ts +++ b/packages/cactus-test-tooling/src/main/typescript/common/containers.ts @@ -308,6 +308,7 @@ export class Containers { cmd: string[], timeoutMs = 300000, // 5 minutes default timeout logLevel: LogLevelDesc = "INFO", + workingDir?: string, ): Promise { const fnTag = "Containers#exec()"; Checks.truthy(container, `${fnTag} container`); @@ -318,12 +319,16 @@ export class Containers { const log = LoggerProvider.getOrCreate({ label: fnTag, level: logLevel }); - const exec = await container.exec({ + const execOptions: Record = { Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: true, - }); + }; + if (workingDir) { + execOptions.WorkingDir = workingDir; + } + const exec = await container.exec(execOptions); return new Promise((resolve, reject) => { log.debug(`Calling Exec Start on Docker Engine API...`); diff --git a/yarn.lock b/yarn.lock index 68dd24b424..859850da1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3776,6 +3776,11 @@ dependencies: "@types/node" "*" +"@types/jsrsasign@8.0.13": + version "8.0.13" + resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-8.0.13.tgz#770c1e429107dfb0cc4f5b78b472584511a55a28" + integrity sha512-+0Ij59D6NXP48KkeLhPXeQKOyLjvA9CD7zacc0Svy2IWHdl62BmDeTvGSIwKaGZSoamLJOo+on1AG/wPRLsd7A== + "@types/keyv@*": version "3.1.2" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5" @@ -3853,6 +3858,13 @@ "@types/ssh2" "*" "@types/ssh2-streams" "*" +"@types/node-vault@0.9.13": + version "0.9.13" + resolved "https://registry.yarnpkg.com/@types/node-vault/-/node-vault-0.9.13.tgz#10d9fe5c9b53995c0ee24fcaeb11ba27241d6c50" + integrity sha512-TQ9zYIzFT4Oo6NVTISk9p4qbuWkVmrs7Rds53uc16sSvQeIC4WGp2y9BXVaKMoGkYQ7jAWlveFTPXLEzuCZffQ== + dependencies: + node-vault "*" + "@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "16.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.2.tgz#0465a39b5456b61a04d98bd5545f8b34be340cb7" @@ -5274,7 +5286,7 @@ bn.js@4.11.6: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" integrity sha1-UzRK2xRhehP26N0s4okF0cC6MhU= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@4.12.0, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -8998,6 +9010,16 @@ fabric-ca-client@2.2.8: url "^0.11.0" winston "^2.4.5" +fabric-ca-client@2.3.0-snapshot.49: + version "2.3.0-snapshot.49" + resolved "https://registry.yarnpkg.com/fabric-ca-client/-/fabric-ca-client-2.3.0-snapshot.49.tgz#7e3beb354c1b3a4548674e8a31707bb7a0238457" + integrity sha512-+JialKe65E5NO3nKOafXDHg5PTXbSq5kmNzuR74EOCVPQBbLzmn+Q6CUDT4V8BC1Nnez2kv2Yziwi44TLMvvpA== + dependencies: + fabric-common "2.3.0-snapshot.50" + jsrsasign "^8.0.24" + url "^0.11.0" + winston "^2.4.5" + fabric-common@2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/fabric-common/-/fabric-common-2.2.8.tgz#efdede0efd6b6362dbc6974511cdda8f24b50bbd" @@ -9017,6 +9039,44 @@ fabric-common@2.2.8: optionalDependencies: pkcs11js "^1.0.6" +fabric-common@2.3.0-snapshot.49: + version "2.3.0-snapshot.49" + resolved "https://registry.yarnpkg.com/fabric-common/-/fabric-common-2.3.0-snapshot.49.tgz#92b03ee299d131cdb61e9f710d09ec2c044195e0" + integrity sha512-CJHMwjBCdP/asH8G5X1/VuumpHdz8U0sWQzmT7fJxiLt8/0bSjPX9rkfd41p8ztK8sqjSBWTk/TZE4o08iyS6Q== + dependencies: + callsite "^1.0.0" + elliptic "^6.5.4" + fabric-protos "2.3.0-snapshot.49" + js-sha3 "^0.8.0" + jsrsasign "^8.0.24" + long "^4.0.0" + nconf "^0.11.2" + promise-settle "^0.3.0" + sjcl "^1.0.8" + winston "^2.4.5" + yn "^4.0.0" + optionalDependencies: + pkcs11js "^1.0.6" + +fabric-common@2.3.0-snapshot.50: + version "2.3.0-snapshot.50" + resolved "https://registry.yarnpkg.com/fabric-common/-/fabric-common-2.3.0-snapshot.50.tgz#37a627e7c2966306dcd237be7fb11ed336dc17c2" + integrity sha512-9M3kWVor5KBmiV+W7Ul14LLYeRiFW39V9+YjJYAiKWMdIUWJjgY8cfwBllnzlEbIe1/Ju+3h1UJZtikc1ZoBww== + dependencies: + callsite "^1.0.0" + elliptic "^6.5.4" + fabric-protos "2.3.0-snapshot.50" + js-sha3 "^0.8.0" + jsrsasign "^8.0.24" + long "^4.0.0" + nconf "^0.11.2" + promise-settle "^0.3.0" + sjcl "^1.0.8" + winston "^2.4.5" + yn "^4.0.0" + optionalDependencies: + pkcs11js "^1.0.6" + fabric-network@2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/fabric-network/-/fabric-network-2.2.8.tgz#e1532faee470f52e39abecbdeb907e248939e6d1" @@ -9027,6 +9087,16 @@ fabric-network@2.2.8: long "^4.0.0" nano "^9.0.3" +fabric-network@2.3.0-snapshot.49: + version "2.3.0-snapshot.49" + resolved "https://registry.yarnpkg.com/fabric-network/-/fabric-network-2.3.0-snapshot.49.tgz#d4bf5f3210feb7c2a866a381f8a2d8ac58af2554" + integrity sha512-rqyBWmzL+TgnpB0PDY8GeLtCiqgwKcMkeg9/I0JgqLpzf/zjMHpDHNx8ToqcBOkGtYnlAHzKCorgCVqDFCyv2g== + dependencies: + fabric-common "2.3.0-snapshot.50" + fabric-protos "2.3.0-snapshot.50" + long "^4.0.0" + nano "^9.0.3" + fabric-protos@2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/fabric-protos/-/fabric-protos-2.2.8.tgz#a41c4d87e6c301bc1064dc12b7e20933f99629bf" @@ -9036,6 +9106,24 @@ fabric-protos@2.2.8: "@grpc/proto-loader" "^0.6.2" protobufjs "^6.11.2" +fabric-protos@2.3.0-snapshot.49: + version "2.3.0-snapshot.49" + resolved "https://registry.yarnpkg.com/fabric-protos/-/fabric-protos-2.3.0-snapshot.49.tgz#650e5cbf08e67e4fece2e07dc9e563d301439bc3" + integrity sha512-zlEm/Xz1MCwEmv+ZA8p6TpOfe2oUxmTeRSM6lNAktZZCYWZDNzTFfXmG1kuYScVgsTH9/WvmVj1HHf6tWdfE1Q== + dependencies: + "@grpc/grpc-js" "^1.3.4" + "@grpc/proto-loader" "^0.6.2" + protobufjs "^6.11.2" + +fabric-protos@2.3.0-snapshot.50: + version "2.3.0-snapshot.50" + resolved "https://registry.yarnpkg.com/fabric-protos/-/fabric-protos-2.3.0-snapshot.50.tgz#d1b6721dacdca997526438dfd9b8510fc612fcab" + integrity sha512-6dRn2sXognFx21SQ9DrHOwZpYj8VphZHLES5COkDRhOnVBs/iDfMptwokcfOOdlDsvx8wmdNjnNMhdPABMDdKA== + dependencies: + "@grpc/grpc-js" "^1.3.4" + "@grpc/proto-loader" "^0.6.2" + protobufjs "^6.11.2" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -12015,6 +12103,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jsrsasign@10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.4.0.tgz#362cc787079c03a363a958c03eb68d8545ba92f7" + integrity sha512-C8qLhiAssh/b74KJpGhWuFGG9cFhJqMCVuuHXRibb3Z5vPuAW0ue0jUirpoExCdpdhv4nD3sZ1DAwJURYJTm9g== + jsrsasign@^8.0.24: version "8.0.24" resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.24.tgz#fc26bac45494caac3dd8f69c1f95847c4bda6c83" @@ -13879,7 +13972,7 @@ node-ssh@12.0.0: shell-escape "^0.2.0" ssh2 "^1.1.0" -node-vault@0.9.22: +node-vault@*, node-vault@0.9.22: version "0.9.22" resolved "https://registry.yarnpkg.com/node-vault/-/node-vault-0.9.22.tgz#052ab9b36c29d80d1ecfad61275259fe710d179e" integrity sha512-/IR+YvINFhCzxJA5x/KHUDymJerFaeqvPUE2zwceRig8yEIA41qfVKusmO6bqRGFkr/2f6CaBVp7YfabzQyteg==