Skip to content

Commit ac17c31

Browse files
committed
feat(express): take walletPassphrase as body parameter for /api/v2/ofc/signPayload
Ticket: GNA-1383
1 parent 47c5eec commit ac17c31

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
@@ -546,6 +546,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
546546
): Promise<{ payload: string; signature: string }> {
547547
const walletId = req.body.walletId;
548548
const payload = req.body.payload;
549+
const bodyWalletPassphrase = req.body.walletPassphrase;
549550
const ofcCoinName = 'ofc';
550551

551552
if (!payload) {
@@ -556,8 +557,8 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
556557
throw new ApiResponseError('Missing required field: walletId', 400);
557558
}
558559

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

562563
const { signerFileSystemPath } = req.config;
563564
if (!signerFileSystemPath) {
@@ -593,6 +594,7 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
593594
export async function handleV2OFCSignPayload(req: express.Request): Promise<{ payload: string; signature: string }> {
594595
const walletId = req.body.walletId;
595596
const payload = req.body.payload;
597+
const bodyWalletPassphrase = req.body.walletPassphrase;
596598
const ofcCoinName = 'ofc';
597599

598600
// 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
628630
throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404);
629631
}
630632

631-
const walletPassphrase = getWalletPwFromEnv(wallet.id());
633+
const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id());
632634
const tradingAccount = wallet.toTradingAccount();
633635
const stringifiedPayload = JSON.stringify(req.body.payload);
634636
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)