Skip to content

Commit

Permalink
feat(express): lightning wallet creation step two
Browse files Browse the repository at this point in the history
initialize signer lnd and update platform.

Ticket: BTC-1356
  • Loading branch information
saravanan7mani committed Sep 2, 2024
1 parent 1d78128 commit 93ad3ab
Show file tree
Hide file tree
Showing 22 changed files with 874 additions and 13 deletions.
5 changes: 4 additions & 1 deletion modules/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"@bitgo/sdk-core": "^28.1.0",
"@bitgo/utxo-lib": "^10.2.0",
"argparse": "^1.0.10",
"bitgo": "^39.1.0",
"bluebird": "^3.5.3",
Expand All @@ -48,7 +49,9 @@
"express": "^4.17.3",
"lodash": "^4.17.20",
"morgan": "^1.9.1",
"superagent": "^9.0.1"
"superagent": "^9.0.1",
"io-ts": "2.1.3",
"io-ts-types": "0.5.16"
},
"devDependencies": {
"@bitgo/public-types": "2.33.4",
Expand Down
4 changes: 4 additions & 0 deletions modules/express/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,8 @@ parser.addArgument(['--signerMode'], {
parser.addArgument(['--signerFileSystemPath'], {
help: 'Local path specifying where an Express signer machine keeps encrypted user private keys.',
});

parser.addArgument(['--lightningSignerFileSystemPath'], {
help: 'Local path specifying where an Express machine keeps lightning signer urls.',
});
export const args = () => parser.parseArgs();
11 changes: 11 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ import { Config } from './config';
import { ApiResponseError } from './errors';
import { promises as fs } from 'fs';
import { retryPromise } from './retryPromise';
import {
handleCreateSignerMacaroon,
handleGetLightningWalletState,
handleInitLightningWallet,
} from './lightning/lightningRoutes';

const { version } = require('bitgo/package.json');
const pjson = require('../package.json');
Expand Down Expand Up @@ -1651,3 +1656,9 @@ export function setupSigningRoutes(app: express.Application, config: Config): vo
promiseWrapper(handleV2OFCSignPayloadInExtSigningMode)
);
}

export function setupLightningRoutes(app: express.Application, config: Config): void {
app.post('/api/v2/:coin/initwallet', parseBody, prepareBitGo(config), promiseWrapper(handleInitLightningWallet));
app.post('/api/v2/:coin/signermacaroon', parseBody, prepareBitGo(config), promiseWrapper(handleCreateSignerMacaroon));
app.get('/api/v2/:coin/wallet/:id/state', prepareBitGo(config), promiseWrapper(handleGetLightningWalletState));
}
6 changes: 5 additions & 1 deletion modules/express/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface Config {
externalSignerUrl?: string;
signerMode?: boolean;
signerFileSystemPath?: string;
lightningSignerFileSystemPath?: string;
}

export const ArgConfig = (args): Partial<Config> => ({
Expand All @@ -59,6 +60,7 @@ export const ArgConfig = (args): Partial<Config> => ({
externalSignerUrl: args.externalSignerUrl,
signerMode: args.signerMode,
signerFileSystemPath: args.signerFileSystemPath,
lightningSignerFileSystemPath: args.lightningSignerFileSystemPath,
});

export const EnvConfig = (): Partial<Config> => ({
Expand All @@ -80,6 +82,7 @@ export const EnvConfig = (): Partial<Config> => ({
externalSignerUrl: readEnvVar('BITGO_EXTERNAL_SIGNER_URL'),
signerMode: readEnvVar('BITGO_SIGNER_MODE') ? true : undefined,
signerFileSystemPath: readEnvVar('BITGO_SIGNER_FILE_SYSTEM_PATH'),
lightningSignerFileSystemPath: readEnvVar('BITGO_LIGHTNING_SIGNER_FILE_SYSTEM_PATH'),
});

export const DefaultConfig: Config = {
Expand All @@ -102,7 +105,7 @@ export const DefaultConfig: Config = {
* @param url
* @return {string}
*/
function _forceSecureUrl(url: string): string {
export function _forceSecureUrl(url: string): string {
const regex = new RegExp(/(^\w+:|^)\/\//);
if (regex.test(url)) {
return url.replace(/(^\w+:|^)\/\//, 'https://');
Expand Down Expand Up @@ -161,6 +164,7 @@ function mergeConfigs(...configs: Partial<Config>[]): Config {
externalSignerUrl,
signerMode: get('signerMode'),
signerFileSystemPath: get('signerFileSystemPath'),
lightningSignerFileSystemPath: get('lightningSignerFileSystemPath'),
};
}

Expand Down
10 changes: 8 additions & 2 deletions modules/express/src/expressApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function createHttpServer(app: express.Application): http.Server {
*/
export function startup(config: Config, baseUri: string): () => void {
return function () {
const { env, ipc, customRootUri, customBitcoinNetwork, signerMode } = config;
const { env, ipc, customRootUri, customBitcoinNetwork, signerMode, lightningSignerFileSystemPath } = config;
/* eslint-disable no-console */
console.log('BitGo-Express running');
console.log(`Environment: ${env}`);
Expand All @@ -122,6 +122,9 @@ export function startup(config: Config, baseUri: string): () => void {
if (signerMode) {
console.log(`External signer mode: ${signerMode}`);
}
if (lightningSignerFileSystemPath) {
console.log(`Lightning signer file system path: ${lightningSignerFileSystemPath}`);
}
/* eslint-enable no-console */
};
}
Expand Down Expand Up @@ -235,6 +238,9 @@ function checkPreconditions(config: Config) {
}

export function setupRoutes(app: express.Application, config: Config): void {
if (config.lightningSignerFileSystemPath) {
clientRoutes.setupLightningRoutes(app, config);
}
if (config.signerMode) {
clientRoutes.setupSigningRoutes(app, config);
} else {
Expand Down Expand Up @@ -308,7 +314,7 @@ export async function prepareIpc(ipcSocketFilePath: string) {
}

export async function init(): Promise<void> {
const cfg = config();
const cfg = await config();
const expressApp = app(cfg);

const server = await createServer(cfg, expressApp);
Expand Down
77 changes: 77 additions & 0 deletions modules/express/src/lightning/codecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as t from 'io-ts';
import { NonEmptyString } from 'io-ts-types';
import { IPCustomCodec } from '@bitgo/sdk-core';

export const WalletStateCodec = t.keyof({
NON_EXISTING: 1,
LOCKED: 1,
UNLOCKED: 1,
RPC_ACTIVE: 1,
SERVER_ACTIVE: 1,
WAITING_TO_START: 1,
});

export type WalletState = t.TypeOf<typeof WalletStateCodec>;

export const LightningSignerConfigCodec = t.type({
url: NonEmptyString,
tlsCert: NonEmptyString,
});

export type LightningSignerConfig = t.TypeOf<typeof LightningSignerConfigCodec>;

export const LightningSignerConfigsCodec = t.record(t.string, LightningSignerConfigCodec);

export type LightningSignerConfigs = t.TypeOf<typeof LightningSignerConfigsCodec>;

export const GetWalletStateResponseCodec = t.type(
{
state: WalletStateCodec,
},
'GetWalletStateResponse'
);

export type GetWalletStateResponse = t.TypeOf<typeof GetWalletStateResponseCodec>;

export const InitLightningWalletRequestCodec = t.strict(
{
walletId: NonEmptyString,
passphrase: NonEmptyString,
signerIP: IPCustomCodec,
signerTlsCert: NonEmptyString,
signerTlsKey: NonEmptyString,
expressIP: IPCustomCodec,
},
'InitLightningWalletRequest'
);

export type InitLightningWalletRequest = t.TypeOf<typeof InitLightningWalletRequestCodec>;

export const CreateSignerMacaroonRequestCodec = t.strict(
{
walletId: NonEmptyString,
passphrase: NonEmptyString,
watchOnlyIP: IPCustomCodec,
},
'CreateSignerMacaroonRequest'
);

export type CreateSignerMacaroonRequest = t.TypeOf<typeof CreateSignerMacaroonRequestCodec>;

export const InitWalletResponseCodec = t.type(
{
admin_macaroon: NonEmptyString,
},
'InitWalletResponse'
);

export type InitWalletResponse = t.TypeOf<typeof InitWalletResponseCodec>;

export const BakeMacaroonResponseCodec = t.type(
{
macaroon: NonEmptyString,
},
'BakeMacaroonResponse'
);

export type BakeMacaroonResponse = t.TypeOf<typeof BakeMacaroonResponseCodec>;
172 changes: 172 additions & 0 deletions modules/express/src/lightning/lightningRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as express from 'express';
import {
decodeOrElse,
createMessageSignature,
getUtxolibNetwork,
signerMacaroonPermissions,
createWatchOnly,
addIPCaveatToMacaroon,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import * as https from 'https';
import { Buffer } from 'buffer';

import { CreateSignerMacaroonRequestCodec, GetWalletStateResponse, InitLightningWalletRequestCodec } from './codecs';
import { getLightningSignerConfig } from './lightningUtils';
import { bakeMacaroon, createHttpAgent, getWalletState, initWallet } from './signerClient';
import { getLightningAuthKeychains, getLightningKeychain, updateWallet } from './lightningWallets';

type Decrypt = (params: { input: string; password: string }) => string;

async function createSignerMacaroon(
config: { url: string; httpsAgent: https.Agent },
header: { adminMacaroonHex: string },
watchOnlyIP: string
) {
const { macaroon } = await bakeMacaroon(config, header, { permissions: signerMacaroonPermissions });
const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIP);
return Buffer.from(macaroonBase64, 'base64').toString('hex');
}

function getSignerRootKey(
passphrase: string,
userMainnetEncryptedPrv: string,
network: utxolib.Network,
decrypt: Decrypt
) {
const userMainnetPrv = decrypt({ password: passphrase, input: userMainnetEncryptedPrv });
return utxolib.bitgo.keyutil.changeExtendedKeyNetwork(userMainnetPrv, utxolib.networks.bitcoin, network);
}

function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, decrypt: Decrypt) {
const hdNode = utxolib.bip32.fromBase58(decrypt({ password: passphrase, input: nodeAuthEncryptedPrv }));
if (!hdNode.privateKey) {
throw new Error('nodeAuthEncryptedPrv is not a private key');
}
return hdNode.privateKey.toString('base64');
}

export async function handleInitLightningWallet(req: express.Request): Promise<unknown> {
const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIP, expressIP } = decodeOrElse(
InitLightningWalletRequestCodec.name,
InitLightningWalletRequestCodec,
req.body,
(_) => {
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
throw new Error('Invalid request body to initialise lightning wallet');
}
);

const { url, tlsCert } = await getLightningSignerConfig(walletId, req.config);

const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
if (coin.getFamily() !== 'lnbtc') {
throw new Error('Invalid coin to initialise lightning wallet');
}

const wallet = await coin.wallets().get({ id: walletId });

const userKey = await getLightningKeychain(coin, wallet);
const { userAuthKey, nodeAuthKey } = await getLightningAuthKeychains(coin, wallet);

const network = getUtxolibNetwork(coin.getChain());
const signerRootKey = getSignerRootKey(passphrase, userKey.encryptedPrv, network, bitgo.decrypt);
const macaroonRootKey = getMacaroonRootKey(passphrase, nodeAuthKey.encryptedPrv, bitgo.decrypt);

const httpsAgent = createHttpAgent(tlsCert);
const { admin_macaroon: adminMacaroon } = await initWallet(
{ url, httpsAgent },
{
wallet_password: passphrase,
extended_master_key: signerRootKey,
macaroon_root_key: macaroonRootKey,
}
);

const encryptedAdminMacaroon = bitgo.encrypt({
password: passphrase,
input: addIPCaveatToMacaroon(adminMacaroon, expressIP),
});
const encryptedSignerTlsKey = bitgo.encrypt({ password: passphrase, input: signerTlsKey });
const watchOnly = createWatchOnly(signerRootKey, network);

const coinSpecific = {
[coin.getChain()]: {
encryptedAdminMacaroon,
signerIP,
signerTlsCert,
encryptedSignerTlsKey,
watchOnly,
},
};

const signature = createMessageSignature(
coinSpecific,
bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv })
);

return await updateWallet(bitgo, wallet, { coinSpecific, signature });
}

export async function handleCreateSignerMacaroon(req: express.Request): Promise<unknown> {
const { walletId, passphrase, watchOnlyIP } = decodeOrElse(
CreateSignerMacaroonRequestCodec.name,
CreateSignerMacaroonRequestCodec,
req.body,
(_) => {
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
throw new Error('Invalid request body for CreateSignerMacaroon.');
}
);

const { url, tlsCert } = await getLightningSignerConfig(walletId, req.config);

const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
if (coin.getFamily() !== 'lnbtc') {
throw new Error('Invalid coin for CreateSignerMacaroon');
}

const wallet = await coin.wallets().get({ id: walletId });

const encryptedAdminMacaroon = wallet.coinSpecific()?.encryptedAdminMacaroon;
if (!encryptedAdminMacaroon) {
throw new Error('Missing encryptedAdminMacaroon in wallet');
}
const adminMacaroon = bitgo.decrypt({ password: passphrase, input: encryptedAdminMacaroon });

const { userAuthKey } = await getLightningAuthKeychains(coin, wallet);

const httpsAgent = createHttpAgent(tlsCert);
const signerMacaroon = await createSignerMacaroon(
{ url, httpsAgent },
{ adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') },
watchOnlyIP
);

// TODO BTC-1465 - Encrypt the signer macaroon using ECDH with the user and LS key pairs
const coinSpecific = {
[coin.getChain()]: {
signerMacaroon,
},
};

const signature = createMessageSignature(
coinSpecific,
bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv })
);

return await updateWallet(bitgo, wallet, { coinSpecific, signature });
}

export async function handleGetLightningWalletState(req: express.Request): Promise<GetWalletStateResponse> {
const coin = req.bitgo.coin(req.params.coin);
if (coin.getFamily() !== 'lnbtc') {
throw new Error('Invalid coin for lightning wallet state');
}

const { url, tlsCert } = await getLightningSignerConfig(req.params.id, req.config);
const httpsAgent = createHttpAgent(tlsCert);
return await getWalletState({ url, httpsAgent });
}
Loading

0 comments on commit 93ad3ab

Please sign in to comment.