Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ export async function handleV2Sign(req: ExpressApiRouteRequest<'express.v2.coin.
}

export async function handleV2OFCSignPayloadInExtSigningMode(
req: express.Request
req: ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>
): Promise<{ payload: string; signature: string }> {
const walletId = req.body.walletId;
const payload = req.body.payload;
Expand Down Expand Up @@ -1741,12 +1741,10 @@ export function setupSigningRoutes(app: express.Application, config: Config): vo
prepareBitGo(config),
promiseWrapper(handleV2GenerateShareTSS)
);
app.post(
`/api/v2/ofc/signPayload`,
parseBody,
router.post('express.v2.ofc.extSignPayload', [
prepareBitGo(config),
promiseWrapper(handleV2OFCSignPayloadInExtSigningMode)
);
typedPromiseWrapper(handleV2OFCSignPayloadInExtSigningMode),
]);
}

export function setupLightningSignerNodeRoutes(app: express.Application, config: Config): void {
Expand Down
4 changes: 4 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { PostConsolidateUnspents } from './v2/consolidateunspents';
import { PostPrebuildAndSignTransaction } from './v2/prebuildAndSignTransaction';
import { PostCoinSign } from './v2/coinSign';
import { PostSendCoins } from './v2/sendCoins';
import { PostOfcExtSignPayload } from './v2/ofcExtSignPayload';

// Too large types can cause the following error
//
Expand Down Expand Up @@ -227,6 +228,9 @@ export const ExpressExternalSigningApiSpec = apiSpec({
'express.v2.coin.sign': {
post: PostCoinSign,
},
'express.v2.ofc.extSignPayload': {
post: PostOfcExtSignPayload,
},
});

export const ExpressWalletSigningApiSpec = apiSpec({
Expand Down
36 changes: 36 additions & 0 deletions modules/express/src/typedRoutes/api/v2/ofcExtSignPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
import { OfcSignPayloadBody, OfcSignPayloadResponse } from './ofcSignPayload';

/**
* Sign an arbitrary payload using an OFC trading account key (External Signing Mode).
*
* This endpoint is used when BitGo Express is running in external signing mode,
* where private keys are stored in an encrypted file on the filesystem rather than
* being fetched from the BitGo API.
*
* The request and response structure is identical to the regular OFC sign payload endpoint,
* but the implementation reads the encrypted private key from a local file specified by
* the `signerFileSystemPath` configuration.
*
* **External Signing Mode Requirements**:
* - `signerFileSystemPath` must be configured in Express config
* - Encrypted private keys must be available in the file system
* - Wallet passphrase must be provided via request body or environment variable
*
* **Flow**:
* 1. Reads encrypted private key from filesystem
* 2. Decrypts private key using wallet passphrase
* 3. Signs the payload with the decrypted key
* 4. Returns signed payload and hex-encoded signature
*
* @operationId express.v2.ofc.extSignPayload
* @tag express
*/
export const PostOfcExtSignPayload = httpRoute({
path: '/api/v2/ofc/signPayload',
method: 'POST',
request: httpRequest({
body: OfcSignPayloadBody,
}),
response: OfcSignPayloadResponse,
});
52 changes: 43 additions & 9 deletions modules/express/test/unit/clientRoutes/signPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'should-http';
import 'should-sinon';
import 'should';
import * as fs from 'fs';
import { Request } from 'express';
import { BitGo, Coin, BaseCoin, Wallet, Wallets } from 'bitgo';
import '../../lib/asserts';
import { handleV2OFCSignPayload, handleV2OFCSignPayloadInExtSigningMode } from '../../../src/clientRoutes';
Expand Down Expand Up @@ -158,10 +157,14 @@ describe('With the handler to sign an arbitrary payload in external signing mode
walletId,
payload,
},
decoded: {
walletId,
payload,
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse);
readFileStub.should.be.calledOnceWith('signerFileSystemPath');
Expand Down Expand Up @@ -195,10 +198,15 @@ describe('With the handler to sign an arbitrary payload in external signing mode
payload,
walletPassphrase: walletPassword,
},
decoded: {
walletId,
payload,
walletPassphrase: walletPassword,
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse);
readFileStub.should.be.calledOnceWith('signerFileSystemPath');
Expand Down Expand Up @@ -233,10 +241,15 @@ describe('With the handler to sign an arbitrary payload in external signing mode
payload,
walletPassphrase: walletPassword,
},
decoded: {
walletId,
payload,
walletPassphrase: walletPassword,
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.resolvedWith(expectedResponse);
readFileStub.should.be.calledOnceWith('signerFileSystemPath');
Expand All @@ -260,7 +273,11 @@ describe('With the handler to sign an arbitrary payload in external signing mode
walletId,
payload,
},
} as unknown as Request;
decoded: {
walletId,
payload,
},
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
'Could not find wallet passphrase WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE in environment'
Expand All @@ -278,10 +295,14 @@ describe('With the handler to sign an arbitrary payload in external signing mode
walletId,
payload,
},
decoded: {
walletId,
payload,
},
config: {
signerFileSystemPath: undefined,
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
'Missing required configuration: signerFileSystemPath'
Expand All @@ -301,10 +322,14 @@ describe('With the handler to sign an arbitrary payload in external signing mode
walletId,
payload,
},
decoded: {
walletId,
payload,
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
"Error when trying to decrypt private key: INVALID: json decode: this isn't json!"
Expand All @@ -326,10 +351,14 @@ describe('With the handler to sign an arbitrary payload in external signing mode
walletId,
payload,
},
decoded: {
walletId,
payload,
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
"Error when trying to decrypt private key: CORRUPT: password error - ccm: tag doesn't match"
Expand All @@ -349,10 +378,15 @@ describe('With the handler to sign an arbitrary payload in external signing mode
payload,
walletPassphrase: 'invalidPassphrase',
},
decoded: {
walletId,
payload,
walletPassphrase: 'invalidPassphrase',
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as Request;
} as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>;

await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith(
"Error when trying to decrypt private key: CORRUPT: password error - ccm: tag doesn't match"
Expand Down
Loading