From ac17c31b9d9470c2200db20218dccac702d3fe44 Mon Sep 17 00:00:00 2001 From: Jonathan Zhao Date: Mon, 3 Mar 2025 15:56:35 -0500 Subject: [PATCH] feat(express): take walletPassphrase as body parameter for /api/v2/ofc/signPayload Ticket: GNA-1383 --- modules/express/EXTERNAL_SIGNER.md | 9 +- modules/express/src/clientRoutes.ts | 8 +- .../test/unit/clientRoutes/signPayload.ts | 98 +++++++++++++++++++ 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/modules/express/EXTERNAL_SIGNER.md b/modules/express/EXTERNAL_SIGNER.md index 654cbb6057..6b43072c7d 100644 --- a/modules/express/EXTERNAL_SIGNER.md +++ b/modules/express/EXTERNAL_SIGNER.md @@ -60,9 +60,14 @@ An example is provided in the file. To run the file, use the command: yarn ts-node ``` -### Wallet passphrase environment variable +### Wallet passphrase +In order for the external signer instance of BitGo Express to decrypt the private key, the wallet passphrase is required. This can be supplied by one of the below methods. -In order for the external signer instance of BitGo Express to decrypt the private key, the wallet passphrase must be set as an environment variable in the format `WALLET__PASSPHRASE`. Note that the wallet passphrase must be set for each wallet. +#### Sending as a body parameter (recommended) +Set the parameter `walletPassphrase: ` (without <>) in POST requests to the endpoint `/api/v2/ofc/signPayload`. + +#### Set as environment variable +Set as an environment variable in the format `WALLET__PASSPHRASE`. Note that the wallet passphrase must be set for each wallet. The environment variable can be set using `export`. For example, the wallet passphrases for the private keys above can be set with the following: ``` diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index c340c43cc4..8cb3ae65a1 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -546,6 +546,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode( ): Promise<{ payload: string; signature: string }> { const walletId = req.body.walletId; const payload = req.body.payload; + const bodyWalletPassphrase = req.body.walletPassphrase; const ofcCoinName = 'ofc'; if (!payload) { @@ -556,8 +557,8 @@ export async function handleV2OFCSignPayloadInExtSigningMode( throw new ApiResponseError('Missing required field: walletId', 400); } - // fetch the password for the given walletId from the env. This is required for decrypting the private key that belongs to that wallet. - const walletPw = getWalletPwFromEnv(walletId); + // fetch the password for the given walletId from the body or the env. This is required for decrypting the private key that belongs to that wallet. + const walletPw = bodyWalletPassphrase || getWalletPwFromEnv(walletId); const { signerFileSystemPath } = req.config; if (!signerFileSystemPath) { @@ -593,6 +594,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode( export async function handleV2OFCSignPayload(req: express.Request): Promise<{ payload: string; signature: string }> { const walletId = req.body.walletId; const payload = req.body.payload; + const bodyWalletPassphrase = req.body.walletPassphrase; const ofcCoinName = 'ofc'; // If the externalSignerUrl is set, forward the request to the express server hosted on the externalSignerUrl @@ -628,7 +630,7 @@ export async function handleV2OFCSignPayload(req: express.Request): Promise<{ pa throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404); } - const walletPassphrase = getWalletPwFromEnv(wallet.id()); + const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id()); const tradingAccount = wallet.toTradingAccount(); const stringifiedPayload = JSON.stringify(req.body.payload); const signature = await tradingAccount.signPayload({ diff --git a/modules/express/test/unit/clientRoutes/signPayload.ts b/modules/express/test/unit/clientRoutes/signPayload.ts index 07fa3bb730..2555a94ffe 100644 --- a/modules/express/test/unit/clientRoutes/signPayload.ts +++ b/modules/express/test/unit/clientRoutes/signPayload.ts @@ -116,6 +116,82 @@ describe('With the handler to sign an arbitrary payload in external signing mode ); readFileStub.restore(); envStub.restore(); + signMessageStub.restore(); + }); + + it('should use wallet passphrase from request body', async () => { + const stubbedSignature = Buffer.from('mysign'); + const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv); + + const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature); + + const stubbedSigHex = stubbedSignature.toString('hex'); + + const expectedResponse = { + payload: JSON.stringify(payload), + signature: stubbedSigHex, + }; + + const req = { + bitgo, + body: { + walletId, + payload, + walletPassphrase: walletPassword, + }, + config: { + signerFileSystemPath: 'signerFileSystemPath', + }, + } as unknown as Request; + + await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse); + readFileStub.should.be.calledOnceWith('signerFileSystemPath'); + signMessageStub.should.be.calledOnceWith( + sinon.match({ + prv: secret, + }) + ); + readFileStub.restore(); + signMessageStub.restore(); + }); + + it('should prioritize request body passphrase over environment variable', async () => { + const stubbedSignature = Buffer.from('mysign'); + const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv); + const envStub = sinon + .stub(process, 'env') + .value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: walletPassword }); + + const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature); + + const stubbedSigHex = stubbedSignature.toString('hex'); + + const expectedResponse = { + payload: JSON.stringify(payload), + signature: stubbedSigHex, + }; + const req = { + bitgo, + body: { + walletId, + payload, + walletPassphrase: walletPassword, + }, + config: { + signerFileSystemPath: 'signerFileSystemPath', + }, + } as unknown as Request; + + await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse); + readFileStub.should.be.calledOnceWith('signerFileSystemPath'); + signMessageStub.should.be.calledOnceWith( + sinon.match({ + prv: secret, + }) + ); + readFileStub.restore(); + envStub.restore(); + signMessageStub.restore(); }); describe('With invalid setup', () => { @@ -206,5 +282,27 @@ describe('With the handler to sign an arbitrary payload in external signing mode readFileStub.restore(); envStub.restore(); }); + + it('should throw error when trying to decrypt with invalid wallet passphrase in body', async () => { + const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv); + + const req = { + bitgo, + body: { + walletId, + payload, + walletPassphrase: 'invalidPassphrase', + }, + config: { + signerFileSystemPath: 'signerFileSystemPath', + }, + } as unknown as Request; + + await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith( + "Error when trying to decrypt private key: CORRUPT: password error - ccm: tag doesn't match" + ); + + readFileStub.restore(); + }); }); });