Skip to content

Commit

Permalink
feat(express): take walletPassphrase as body parameter for /api/v2/of…
Browse files Browse the repository at this point in the history
…c/signPayload

Ticket: GNA-1383
  • Loading branch information
jzhao-bitgo committed Mar 4, 2025
1 parent 47c5eec commit ac17c31
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 @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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({
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 ac17c31

Please sign in to comment.