Skip to content

Commit 57c92eb

Browse files
authored
Merge pull request #5679 from BitGo/GNA-1383
feat(express): take walletPassphrase as body parameter
2 parents 59399d5 + ac17c31 commit 57c92eb

File tree

3 files changed

+110
-5
lines changed

3 files changed

+110
-5
lines changed

modules/express/EXTERNAL_SIGNER.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ An example is provided in the file. To run the file, use the command:
6060
yarn ts-node <path/to/fetchEncryptedPrivKeys.ts>
6161
```
6262

63-
### Wallet passphrase environment variable
63+
### Wallet passphrase
64+
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.
6465

65-
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_<walletId>_PASSPHRASE`. Note that the wallet passphrase must be set for each wallet.
66+
#### Sending as a body parameter (recommended)
67+
Set the parameter `walletPassphrase: <YOUR_WALLET_PASSPHRASE>` (without <>) in POST requests to the endpoint `/api/v2/ofc/signPayload`.
68+
69+
#### Set as environment variable
70+
Set as an environment variable in the format `WALLET_<walletId>_PASSPHRASE`. Note that the wallet passphrase must be set for each wallet.
6671
The environment variable can be set using `export`. For example, the wallet passphrases for the private keys above can be set with the following:
6772

6873
```

modules/express/src/clientRoutes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
552552
): Promise<{ payload: string; signature: string }> {
553553
const walletId = req.body.walletId;
554554
const payload = req.body.payload;
555+
const bodyWalletPassphrase = req.body.walletPassphrase;
555556
const ofcCoinName = 'ofc';
556557

557558
if (!payload) {
@@ -562,8 +563,8 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
562563
throw new ApiResponseError('Missing required field: walletId', 400);
563564
}
564565

565-
// fetch the password for the given walletId from the env. This is required for decrypting the private key that belongs to that wallet.
566-
const walletPw = getWalletPwFromEnv(walletId);
566+
// 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.
567+
const walletPw = bodyWalletPassphrase || getWalletPwFromEnv(walletId);
567568

568569
const { signerFileSystemPath } = req.config;
569570
if (!signerFileSystemPath) {
@@ -599,6 +600,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
599600
export async function handleV2OFCSignPayload(req: express.Request): Promise<{ payload: string; signature: string }> {
600601
const walletId = req.body.walletId;
601602
const payload = req.body.payload;
603+
const bodyWalletPassphrase = req.body.walletPassphrase;
602604
const ofcCoinName = 'ofc';
603605

604606
// If the externalSignerUrl is set, forward the request to the express server hosted on the externalSignerUrl
@@ -634,7 +636,7 @@ export async function handleV2OFCSignPayload(req: express.Request): Promise<{ pa
634636
throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404);
635637
}
636638

637-
const walletPassphrase = getWalletPwFromEnv(wallet.id());
639+
const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id());
638640
const tradingAccount = wallet.toTradingAccount();
639641
const stringifiedPayload = JSON.stringify(req.body.payload);
640642
const signature = await tradingAccount.signPayload({

modules/express/test/unit/clientRoutes/signPayload.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,82 @@ describe('With the handler to sign an arbitrary payload in external signing mode
116116
);
117117
readFileStub.restore();
118118
envStub.restore();
119+
signMessageStub.restore();
120+
});
121+
122+
it('should use wallet passphrase from request body', async () => {
123+
const stubbedSignature = Buffer.from('mysign');
124+
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
125+
126+
const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature);
127+
128+
const stubbedSigHex = stubbedSignature.toString('hex');
129+
130+
const expectedResponse = {
131+
payload: JSON.stringify(payload),
132+
signature: stubbedSigHex,
133+
};
134+
135+
const req = {
136+
bitgo,
137+
body: {
138+
walletId,
139+
payload,
140+
walletPassphrase: walletPassword,
141+
},
142+
config: {
143+
signerFileSystemPath: 'signerFileSystemPath',
144+
},
145+
} as unknown as Request;
146+
147+
await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse);
148+
readFileStub.should.be.calledOnceWith('signerFileSystemPath');
149+
signMessageStub.should.be.calledOnceWith(
150+
sinon.match({
151+
prv: secret,
152+
})
153+
);
154+
readFileStub.restore();
155+
signMessageStub.restore();
156+
});
157+
158+
it('should prioritize request body passphrase over environment variable', async () => {
159+
const stubbedSignature = Buffer.from('mysign');
160+
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
161+
const envStub = sinon
162+
.stub(process, 'env')
163+
.value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: walletPassword });
164+
165+
const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature);
166+
167+
const stubbedSigHex = stubbedSignature.toString('hex');
168+
169+
const expectedResponse = {
170+
payload: JSON.stringify(payload),
171+
signature: stubbedSigHex,
172+
};
173+
const req = {
174+
bitgo,
175+
body: {
176+
walletId,
177+
payload,
178+
walletPassphrase: walletPassword,
179+
},
180+
config: {
181+
signerFileSystemPath: 'signerFileSystemPath',
182+
},
183+
} as unknown as Request;
184+
185+
await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse);
186+
readFileStub.should.be.calledOnceWith('signerFileSystemPath');
187+
signMessageStub.should.be.calledOnceWith(
188+
sinon.match({
189+
prv: secret,
190+
})
191+
);
192+
readFileStub.restore();
193+
envStub.restore();
194+
signMessageStub.restore();
119195
});
120196

121197
describe('With invalid setup', () => {
@@ -206,5 +282,27 @@ describe('With the handler to sign an arbitrary payload in external signing mode
206282
readFileStub.restore();
207283
envStub.restore();
208284
});
285+
286+
it('should throw error when trying to decrypt with invalid wallet passphrase in body', async () => {
287+
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
288+
289+
const req = {
290+
bitgo,
291+
body: {
292+
walletId,
293+
payload,
294+
walletPassphrase: 'invalidPassphrase',
295+
},
296+
config: {
297+
signerFileSystemPath: 'signerFileSystemPath',
298+
},
299+
} as unknown as Request;
300+
301+
await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
302+
"Error when trying to decrypt private key: CORRUPT: password error - ccm: tag doesn't match"
303+
);
304+
305+
readFileStub.restore();
306+
});
209307
});
210308
});

0 commit comments

Comments
 (0)