Skip to content

Commit

Permalink
Merge pull request #4898 from BitGo/BTC-1465-encrypt-signer-macaroon
Browse files Browse the repository at this point in the history
feat(express): encrypt signer macaroon using ecdh
  • Loading branch information
saravanan7mani authored Sep 11, 2024
2 parents 2168100 + 62cbd00 commit 4558de3
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 29 deletions.
16 changes: 10 additions & 6 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
updateLightningWallet,
LightningWalletCoinSpecific,
isLightningCoinName,
deriveLightningServiceSharedSecret,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import { Buffer } from 'buffer';
Expand All @@ -32,7 +33,6 @@ async function createSignerMacaroon(
) {
const { macaroon } = await lndSignerClient.bakeMacaroon({ permissions: signerMacaroonPermissions }, header);
const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIP);
// TODO BTC-1465 - Encrypt the signer macaroon using ECDH with the user and LS key pairs
return Buffer.from(macaroonBase64, 'base64').toString('hex');
}

Expand Down Expand Up @@ -154,12 +154,19 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<

const { userAuthKey } = await getLightningAuthKeychains(wallet);

const encryptedSignerMacaroon = await createSignerMacaroon(
const signerMacaroon = await createSignerMacaroon(
watchOnlyIP,
{ adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') },
lndSignerClient
);

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

const encryptedSignerMacaroon = bitgo.encrypt({
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
input: signerMacaroon,
});

const coinSpecific = {
[coin.getChain()]: {
encryptedSignerMacaroon,
Expand All @@ -170,10 +177,7 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
throw new Error('Invalid lightning wallet coin specific data');
}

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

return await updateLightningWallet(wallet, { coinSpecific, signature });
}
Expand Down
25 changes: 21 additions & 4 deletions modules/sdk-core/src/bitgo/lightning/lightningUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as utxolib from '@bitgo/utxo-lib';
import { importMacaroon, bytesToBase64 } from 'macaroon';
import * as bs58check from 'bs58check';
import { WatchOnly, WatchOnlyAccount } from './codecs';
import { getSharedSecret } from '../ecdh';

// https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md#the-signer-node
export const signerMacaroonPermissions = [
Expand Down Expand Up @@ -63,18 +64,24 @@ export function isValidLightningNetwork(network: unknown): network is utxolib.Ne
}

/**
* Returns the utxolib network name for a lightning coin.
* Returns the statics network data for a lightning coin.
*/
export function getUtxolibNetworkName(coinName: string): string | undefined {
export function getStaticsLightningNetwork(coinName: string): statics.LightningNetwork {
if (!isLightningCoinName(coinName)) {
throw new Error(`${coinName} is not a lightning coin`);
}
const coin = statics.coins.get(coinName);
return coin instanceof statics.LightningCoin ? coin.network.utxolibName : undefined;
if (!(coin instanceof statics.LightningCoin)) {
throw new Error('coin is not a lightning coin');
}
return coin.network;
}

/**
* Returns the utxolib network for a lightning coin.
*/
export function getUtxolibNetwork(coinName: string): utxolib.Network {
const networkName = getUtxolibNetworkName(coinName);
const networkName = getStaticsLightningNetwork(coinName).utxolibName;
if (!isValidLightningNetworkName(networkName)) {
throw new Error('invalid lightning network');
}
Expand Down Expand Up @@ -189,3 +196,13 @@ export function createWatchOnly(signerRootKey: string, network: utxolib.Network)
const accounts = deriveWatchOnlyAccounts(masterHDNode, utxolib.isMainnet(network));
return { master_key_birthday_timestamp, master_key_fingerprint, accounts };
}

/**
* Derives the shared Elliptic Curve Diffie-Hellman (ECDH) secret between the user's auth extended private key
* and the Lightning service's public key for secure communication.
*/
export function deriveLightningServiceSharedSecret(coinName: 'lnbtc' | 'tlnbtc', userAuthXprv: string): Buffer {
const publicKey = Buffer.from(getStaticsLightningNetwork(coinName).lightningServicePubKey, 'hex');
const userAuthHdNode = utxolib.bip32.fromBase58(userAuthXprv);
return getSharedSecret(userAuthHdNode, publicKey);
}
62 changes: 44 additions & 18 deletions modules/sdk-core/test/unit/bitgo/lightning/lightningUtils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import assert from 'assert';
import * as utxolib from '@bitgo/utxo-lib';
import * as sinon from 'sinon';
import { importMacaroon } from 'macaroon';
import * as statics from '@bitgo/statics';
import * as utxolib from '@bitgo/utxo-lib';

import { accounts, signerRootKey } from './createWatchOnlyFixture';
import {
addIPCaveatToMacaroon,
createWatchOnly,
isValidLightningNetworkName,
getLightningNetwork,
isValidLightningNetwork,
getStaticsLightningNetwork,
getUtxolibNetwork,
getUtxolibNetworkName,
isLightningCoinName,
isValidLightningNetwork,
isValidLightningNetworkName,
} from '../../../../src';
import { accounts, signerRootKey } from './createWatchOnlyFixture';
import { networks } from '@bitgo/utxo-lib';
createWatchOnly,
addIPCaveatToMacaroon,
deriveLightningServiceSharedSecret,
} from './../../../../src/bitgo/lightning/lightningUtils';

import * as lightningUtils from '../../../../src/bitgo/lightning/lightningUtils';
import { getSharedSecret } from '../../../../src';

describe('lightning utils', function () {
[
Expand All @@ -32,8 +37,8 @@ describe('lightning utils', function () {
assert(isValidLightningNetwork(utxolib.networks[networkName]));
});

it(`getUtxolibNetworkName`, function () {
assert.strictEqual(getUtxolibNetworkName(name), networkName);
it(`getStaticsLightningNetwork`, function () {
assert.strictEqual(getStaticsLightningNetwork(name).family, 'lnbtc');
});

it(`getUtxolibNetwork`, function () {
Expand All @@ -58,22 +63,18 @@ describe('lightning utils', function () {
assert.strictEqual(isValidLightningNetwork(utxolib.networks['litecoin']), false);
});

it(`getUtxolibNetworkName should return undefined for non lightning coin`, function () {
assert.strictEqual(getUtxolibNetworkName('ltc'), undefined);
});

it(`getUtxolibNetwork should return fail for invalid lightning coin`, function () {
assert.throws(() => {
getUtxolibNetwork('ltc');
}, /invalid lightning network/);
}, /ltc is not a lightning coin/);
});

it(`createWatchOnly`, function () {
const watchOnly = createWatchOnly(signerRootKey, networks.testnet);
const watchOnly = createWatchOnly(signerRootKey, utxolib.networks.testnet);
assert.deepStrictEqual(watchOnly.accounts, accounts);
assert.strictEqual(
watchOnly.master_key_fingerprint,
utxolib.bip32.fromBase58(signerRootKey, networks.testnet).fingerprint.toString('hex')
utxolib.bip32.fromBase58(signerRootKey, utxolib.networks.testnet).fingerprint.toString('hex')
);
});

Expand All @@ -86,4 +87,29 @@ describe('lightning utils', function () {
const macaroonObjWithCaveat = importMacaroon(macaroonWithCaveat).exportJSON();
assert.strictEqual(macaroonObjWithCaveat.c[0].i, 'ipaddr 127.0.0.1');
});

it(`deriveLightningServiceSharedSecret`, function () {
const userAuthXprv =
'xprv9s21ZrQH143K4NPkV8riiTnFf72MRyQDVHMmmpekGF1w5QkS2MfTei9KXYvrZVMop4zQ4arnzSF7TRp3Cy73AWaDdADiYMCi5qpYW1bUa5m';
const lightningServicePubKey = '03b6fe266b3f8ae110b877d942765e9cea9e82faf03cdbb6d0effe980b6371b9c2';
const lightningServicePrvKey = '8b95613f4341e347743bd2625728d87bc6f0a119acb6ae9121afeee2b2a650f7';

const coin = statics.coins.get('tlnbtc');
assert(coin instanceof statics.LightningCoin);

const getStaticsLightningNetworkStub = sinon.stub(lightningUtils, 'getStaticsLightningNetwork').returns({
...coin.network,
lightningServicePubKey,
});

const secret = deriveLightningServiceSharedSecret('tlnbtc', userAuthXprv);
getStaticsLightningNetworkStub.restore();

const expectedSecret = getSharedSecret(
Buffer.from(lightningServicePrvKey, 'hex'),
utxolib.bip32.fromBase58(userAuthXprv).neutered()
);

assert.deepStrictEqual(secret, expectedSecret);
});
});
13 changes: 12 additions & 1 deletion modules/statics/src/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ export interface UtxoNetwork extends BaseNetwork {
utxolibName: string;
}

export type LightningNetwork = UtxoNetwork;
export interface LightningNetwork extends UtxoNetwork {
/**
* The public key of the Lightning service, used for deriving the shared Elliptic Curve Diffie-Hellman (ECDH) secret
* between the user's extended private key and the Lightning service. This key facilitates secure communication
* by enabling the creation of a shared secret for encryption and decryption of data.
*/
lightningServicePubKey: string;
}

export interface AdaNetwork extends BaseNetwork {
// Network name as defined in @bitgo/utxo-lib networks.ts
Expand Down Expand Up @@ -271,13 +278,17 @@ class LightningBitcoin extends Mainnet implements LightningNetwork {
family = CoinFamily.LNBTC;
utxolibName = 'bitcoin';
explorerUrl = 'https://mempool.space/lightning';
// TODO BTC-1423 - Currently dummy key is used here to unblock development. Replace with the actual public key once the ticket is done.
lightningServicePubKey = '039c67c461dc751b32b983075210875c388bbb918d7b88c31e1a5a3164d693cf41';
}

class LightningBitcoinTestnet extends Testnet implements LightningNetwork {
name = 'LightningBitcoinTestnet';
family = CoinFamily.LNBTC;
utxolibName = 'testnet';
explorerUrl = 'https://mempool.space/testnet/lightning';
// TODO BTC-1423 - Currently dummy key is used here to unblock development. Replace with the actual public key once the ticket is done.
lightningServicePubKey = '03b6fe266b3f8ae110b877d942765e9cea9e82faf03cdbb6d0effe980b6371b9c2';
}

class Bitcoin extends Mainnet implements UtxoNetwork {
Expand Down

0 comments on commit 4558de3

Please sign in to comment.