Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ coverage
.claude/agents/*
!.claude/agents/*.md
.cursor/
# Prevent committing encrypted private key fixtures generated by fetchEncryptedPrivKeys.ts
encryptedPrivKeys.json
modules/express/encryptedPrivKeys.json
8 changes: 4 additions & 4 deletions modules/express/EXTERNAL_SIGNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ Here is an example json file containing two wallet IDs and their corresponding e

```
{
"61f039aad587c2000745c687373e0fa9":"{\"iv\":\"+1u1Y9cvsYuRMeyH2slnXQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"54kOXTqJ9mc=\",\"ct\":\"JF5wQ82wa1dYyFxFlbHCvK4a+A6MTHdhOqc5uXsz2icWhkY2Lin/3Ab8ZwvwDaR1JYKmC/g1gXIGwVZEOl1M/bRHY420h7sDtmTS6Ebse5NWbF0ItfUJlk6HVATGa+C6mkbaVxJ4kQW/ehnT3riqzU069ATPz8E=\"}",
"61fb21819c54dd000755f8de3a18e46f":"{\"iv\":\"ULAkh1Ia2B2oJbVWRt+xMw==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"SVkVei5M1qU=\",\"ct\":\"NxfG1HQWGcrwCHkQh8DKeMaZrRic+SSBQHtuOSsSJzW5MDOpwqDta8PDdh52lp9eqtaY+CGN6rPhaGbeZDrEyV2PoBGeb48GicMTVAehkyoF9mr8edtsWDCxcmmde+1zv3czy2n/bgXYNGvX39D30GDRpfovSYc=\"}"
"<walletId1>":"<encryptedPrivateKey1>",
"<walletId2>":"<encryptedPrivateKey2>"
}
```

Expand Down Expand Up @@ -71,8 +71,8 @@ Set as an environment variable in the format `WALLET_<walletId>_PASSPHRASE`. Not
The environment variable can be set using `export`. For example, the wallet passphrases for the private keys above can be set with the following:

```
export WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE=wDX058%c4plL1@pP
export WALLET_61fb21819c54dd000755f8de3a18e46f_PASSPHRASE=wDX058%c4plL1@pP
export WALLET_<walletId1>_PASSPHRASE=<YOUR_WALLET_PASSPHRASE>
export WALLET_<walletId2>_PASSPHRASE=<YOUR_WALLET_PASSPHRASE>
```

### External signer mode configuration values
Expand Down
3 changes: 0 additions & 3 deletions modules/express/encryptedPrivKeys.json

This file was deleted.

2 changes: 1 addition & 1 deletion modules/express/test/unit/bitgoExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ proxyquire.noPreserveCache();
describe('Bitgo Express', function () {
describe('server initialization', function () {
const validPrvJSON =
'{"61f039aad587c2000745c687373e0fa9":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2"}';
'{"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2"}';
const validLightningSignerConfigJSON = '{"fakeid":{"url": "https://127.0.0.1:8080","tlsCert":"dummy"}}';

it('should require NODE_ENV to be production when running against prod env', function () {
Expand Down
18 changes: 11 additions & 7 deletions modules/express/test/unit/clientRoutes/externalSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,11 @@ describe('External signer', () => {
let mpcEcdsa: Ecdsa;
let hdTree: HDTree;

const walletId = '61f039aad587c2000745c687373e0fa9';
const walletPassword = 'wDX058%c4plL1@pP';
const walletId = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';
const walletPassword = 'test-wallet-passphrase';
const secret =
'xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2';
const validPrv =
'{"61f039aad587c2000745c687373e0fa9":"{\\"iv\\":\\"+1u1Y9cvsYuRMeyH2slnXQ==\\",\\"v\\":1,\\"iter\\":10000,\\"ks\\":256,\\"ts\\":64,\\"mode\\":\\"ccm\\",\\"adata\\":\\"\\",\\"cipher\\":\\"aes\\",\\"salt\\":\\"54kOXTqJ9mc=\\",\\"ct\\":\\"JF5wQ82wa1dYyFxFlbHCvK4a+A6MTHdhOqc5uXsz2icWhkY2Lin/3Ab8ZwvwDaR1JYKmC/g1gXIGwVZEOl1M/bRHY420h7sDtmTS6Ebse5NWbF0ItfUJlk6HVATGa+C6mkbaVxJ4kQW/ehnT3riqzU069ATPz8E=\\"}"}';
let validPrv: string;

before(async function () {
if (!nock.isActive()) {
Expand All @@ -99,6 +98,13 @@ describe('External signer', () => {
bitgo = TestBitGo.decorate(BitGo, { env: 'test' });
bitgo.initializeTestVars();

const encryptedPrv = await bitgo.encrypt({
password: walletPassword,
input: secret,
encryptionVersion: 1,
});
validPrv = JSON.stringify({ [walletId]: encryptedPrv });

bgUrl = common.Environments[bitgo.getEnv()].uri;
hdTree = await Ed25519BIP32.initialize();
MPC = await Eddsa.initialize(hdTree);
Expand All @@ -122,9 +128,7 @@ describe('External signer', () => {

it('should read an encrypted prv from signerFileSystemPath and pass it to coin.signTransaction', async () => {
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
const envStub = sinon
.stub(process, 'env')
.value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: walletPassword });
const envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: walletPassword });
const signTransactionStub = sinon
.stub(Coin.Btc.prototype, 'signTransaction')
.resolves({ txHex: 'signedTx', txRequestId: '' } as SignedTransaction);
Expand Down
39 changes: 17 additions & 22 deletions modules/express/test/unit/clientRoutes/signPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,11 @@ describe('Sign an arbitrary payload with trading account key', function () {
describe('With the handler to sign an arbitrary payload in external signing mode', () => {
let bitgo: BitGo;

const walletId = '61f039aad587c2000745c687373e0fa9';
const walletPassword = 'wDX058%c4plL1@pP';
const walletId = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';
const walletPassword = 'test-wallet-passphrase';
const secret =
'xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2';
const validPrv =
'{"61f039aad587c2000745c687373e0fa9":"{\\"iv\\":\\"+1u1Y9cvsYuRMeyH2slnXQ==\\",\\"v\\":1,\\"iter\\":10000,\\"ks\\":256,\\"ts\\":64,\\"mode\\":\\"ccm\\",\\"adata\\":\\"\\",\\"cipher\\":\\"aes\\",\\"salt\\":\\"54kOXTqJ9mc=\\",\\"ct\\":\\"JF5wQ82wa1dYyFxFlbHCvK4a+A6MTHdhOqc5uXsz2icWhkY2Lin/3Ab8ZwvwDaR1JYKmC/g1gXIGwVZEOl1M/bRHY420h7sDtmTS6Ebse5NWbF0ItfUJlk6HVATGa+C6mkbaVxJ4kQW/ehnT3riqzU069ATPz8E=\\"}"}';
let validPrv: string;

const payload = {
this: {
Expand All @@ -157,16 +156,20 @@ describe('With the handler to sign an arbitrary payload in external signing mode
},
};

before(() => {
before(async () => {
bitgo = new BitGo({ env: 'test' });
const encryptedPrv = await bitgo.encrypt({
password: walletPassword,
input: secret,
encryptionVersion: 1,
});
validPrv = JSON.stringify({ [walletId]: encryptedPrv });
});

it('should return a payload signed with trading account key read from the local file system', 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 envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: walletPassword });

const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature);

Expand Down Expand Up @@ -248,9 +251,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode
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 envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: walletPassword });

const signMessageStub = sinon.stub(Coin.Ofc.prototype, 'signMessage').resolves(stubbedSignature);

Expand Down Expand Up @@ -290,7 +291,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode
});

describe('With invalid setup', () => {
const invalidPrv = '{"61f039aad587c2000745c687373e0fa9":"invalid"}';
const invalidPrv = JSON.stringify({ [walletId]: 'invalid' });

it('should throw an error with missing wallet passphrase in env', async () => {
const req = {
Expand All @@ -306,14 +307,12 @@ describe('With the handler to sign an arbitrary payload in external signing mode
} 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'
`Could not find wallet passphrase WALLET_${walletId}_PASSPHRASE in environment`
);
});

it('should throw an error with undefined signerFileSystemPath in env', async () => {
const envStub = sinon
.stub(process, 'env')
.value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: walletPassword });
const envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: walletPassword });

const req = {
bitgo,
Expand All @@ -338,9 +337,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode

it('should throw error when trying to decrypt with invalid private key', async () => {
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(invalidPrv);
const envStub = sinon
.stub(process, 'env')
.value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: walletPassword });
const envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: walletPassword });

const req = {
bitgo,
Expand All @@ -367,9 +364,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode

it('should throw error when trying to decrypt with invalid wallet passphrase key', async () => {
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
const envStub = sinon
.stub(process, 'env')
.value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: 'invalidPassphrase' });
const envStub = sinon.stub(process, 'env').value({ [`WALLET_${walletId}_PASSPHRASE`]: 'invalidPassphrase' });

const req = {
bitgo,
Expand Down
24 changes: 7 additions & 17 deletions modules/express/test/unit/typedRoutes/coinSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,32 @@ describe('CoinSign codec tests (External Signer Mode)', function () {
const decryptedPrivKey =
'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2';
const walletPassphrase = 'test_wallet_passphrase';
// Use the existing encryptedPrivKeys.json file in the project root
const path = require('path');
const signerFilePath = path.join(__dirname, '../../../encryptedPrivKeys.json');
const os = require('os');
const signerFilePath = path.join(os.tmpdir(), `encryptedPrivKeys-${process.pid}-${Date.now()}.json`);

let fsReadFileStub: sinon.SinonStub;
let agent: ReturnType<typeof setupAgent>;
let originalFileContent: string;

// Mock encrypted private keys JSON content
const mockSignerFileContent = JSON.stringify({
[walletId]: encryptedPrivKey,
});

// Setup the express app with signer mode before all tests
before(function () {
// Save the original content of encryptedPrivKeys.json
try {
originalFileContent = fsSync.readFileSync(signerFilePath, 'utf8');
} catch (e) {
originalFileContent = '{}';
}

// Temporarily write mock data to the existing file
fsSync.writeFileSync(signerFilePath, mockSignerFileContent);

// Create agent with signerMode enabled for external signing
agent = setupAgent({
signerMode: true,
signerFileSystemPath: signerFilePath,
});
});

// Restore the original file content after all tests
after(function () {
// Restore original content
fsSync.writeFileSync(signerFilePath, originalFileContent);
try {
fsSync.unlinkSync(signerFilePath);
} catch (e) {
// ignore
}
});

beforeEach(function () {
Expand Down
23 changes: 7 additions & 16 deletions modules/express/test/unit/typedRoutes/generateShareTSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,40 +39,31 @@ describe('GenerateShareTSS codec tests (External Signer Mode)', function () {
'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2';
const walletPassphrase = 'test_wallet_passphrase';
const path = require('path');
const signerFilePath = path.join(__dirname, '../../../encryptedPrivKeys.json');
const os = require('os');
const signerFilePath = path.join(os.tmpdir(), `encryptedPrivKeys-${process.pid}-${Date.now()}.json`);

let fsReadFileStub: sinon.SinonStub;
let agent: ReturnType<typeof setupAgent>;
let originalFileContent: string;

// Mock encrypted private keys JSON content
const mockSignerFileContent = JSON.stringify({
[walletId]: encryptedPrivKey,
});

// Setup the express app with signer mode before all tests
before(function () {
// Save the original content of encryptedPrivKeys.json
try {
originalFileContent = fsSync.readFileSync(signerFilePath, 'utf8');
} catch (e) {
originalFileContent = '{}';
}

// Temporarily write mock data to the existing file
fsSync.writeFileSync(signerFilePath, mockSignerFileContent);

// Create agent with signerMode enabled for external signing
agent = setupAgent({
signerMode: true,
signerFileSystemPath: signerFilePath,
});
});

// Restore the original file content after all tests
after(function () {
// Restore original content
fsSync.writeFileSync(signerFilePath, originalFileContent);
try {
fsSync.unlinkSync(signerFilePath);
} catch (e) {
// ignore
}
});

beforeEach(function () {
Expand Down
16 changes: 7 additions & 9 deletions modules/express/test/unit/typedRoutes/ofcExtSignPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ describe('OfcExtSignPayload External Signer Mode Tests', function () {
const walletPassphrase = 'test_wallet_passphrase_ofc';

const path = require('path');
const signerFilePath = path.join(__dirname, '../../../encryptedPrivKeys.json');
const os = require('os');
const signerFilePath = path.join(os.tmpdir(), `encryptedPrivKeys-${process.pid}-${Date.now()}.json`);

let fsReadFileStub: sinon.SinonStub;
let agent: ReturnType<typeof setupAgent>;
let originalFileContent: string;

const mockSignerFileContent = JSON.stringify({
[walletId]: encryptedPrivKey,
Expand All @@ -36,12 +36,6 @@ describe('OfcExtSignPayload External Signer Mode Tests', function () {
};

before(function () {
try {
originalFileContent = fsSync.readFileSync(signerFilePath, 'utf8');
} catch (e) {
originalFileContent = '{}';
}

fsSync.writeFileSync(signerFilePath, mockSignerFileContent);

agent = setupAgent({
Expand All @@ -51,7 +45,11 @@ describe('OfcExtSignPayload External Signer Mode Tests', function () {
});

after(function () {
fsSync.writeFileSync(signerFilePath, originalFileContent);
try {
fsSync.unlinkSync(signerFilePath);
} catch (e) {
// ignore
}
});

beforeEach(function () {
Expand Down
Loading