Skip to content

Commit

Permalink
Merge pull request #5679 from BitGo/GNA-1383
Browse files Browse the repository at this point in the history
feat(express): take walletPassphrase as body parameter
  • Loading branch information
pranavjain97 authored Mar 6, 2025
2 parents 59399d5 + ac17c31 commit 57c92eb
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
9 changes: 7 additions & 2 deletions modules/express/EXTERNAL_SIGNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ An example is provided in the file. To run the file, use the command:
yarn ts-node <path/to/fetchEncryptedPrivKeys.ts>
```

### 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_<walletId>_PASSPHRASE`. Note that the wallet passphrase must be set for each wallet.
#### Sending as a body parameter (recommended)
Set the parameter `walletPassphrase: <YOUR_WALLET_PASSPHRASE>` (without <>) in POST requests to the endpoint `/api/v2/ofc/signPayload`.

#### Set as environment variable
Set as an environment variable in the format `WALLET_<walletId>_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:

```
Expand Down
8 changes: 5 additions & 3 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,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) {
Expand All @@ -562,8 +563,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) {
Expand Down Expand Up @@ -599,6 +600,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
Expand Down Expand Up @@ -634,7 +636,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({
Expand Down
98 changes: 98 additions & 0 deletions modules/express/test/unit/clientRoutes/signPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});

0 comments on commit 57c92eb

Please sign in to comment.