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
18 changes: 17 additions & 1 deletion modules/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ BitGo Express is able to take configuration options from either command line arg
| N/A | --disableproxy | `BITGO_DISABLE_PROXY` <sup>0</sup> | N/A | Disable proxying of routes not explicitly handled by bitgo-express |
| N/A | --disableenvcheck | `BITGO_DISABLE_ENV_CHECK` <sup>0</sup> | N/A | Disable checking for correct `NODE_ENV` environment variable when running against BitGo production environment. |
| -i | --ipc | `BITGO_IPC` | N/A | If set, bind to the given IPC (unix domain) socket. Binding to an IPC socket can be useful if the caller of bitgo-express resides on the same host as the bitgo-express instance itself, since the socket can be secured using normal file permissions and ownership semantics. Note: This is not supported on Windows platforms. |
| N/A | --authversion | `BITGO_AUTH_VERSION` | 2 | BitGo Authentication scheme version which should be used form making requests to the BitGo server. For more info on authentication scheme versions, view the API reference on the BitGo [Developer Portal](https://developers.bitgo.com/reference/overview#/). |
| N/A | --authversion | `BITGO_AUTH_VERSION` | 2 | BitGo Authentication scheme version which should be used form making requests to the BitGo server. For more info on authentication scheme versions, view the API reference on the BitGo [Developer Portal](https://developers.bitgo.com/reference/overview#/). |
| N/A | --externalSignerUrl | `BITGO_EXTERNAL_SIGNER_URL` | N/A | URL specifying the external API to call for remote signing. |
| N/A | --signerMode | `BITGO_SIGNER_MODE ` | N/A | If set, run Express as a remote signer. |
| N/A | --signerFileSystemPath | `BITGO_SIGNER_FILE_SYSTEM_PATH ` | N/A | Local path specifying where an Express signer machine keeps the encrypted user private keys. Required when signerMode is set. |
Expand Down Expand Up @@ -265,6 +265,22 @@ To enable the `bitgo:v2:utxo` and `bitgo:express` debug namespaces, start BitGo

Wildcards using `*` are also supported. For example, all bitgo debug namespaces can be enabled with `--debug-namespace bitgo:*`, but beware, this can be very noisy.

## Wallet Safes (client-side generation)

Safe creation and child-wallet minting run local key ceremonies, so they must be called through BitGo Express rather than the BitGo API. The passphrase never leaves the Express host. Production Express requires TLS.

| Method | Path | SDK method |
| ------ | ------------------------------------------------------------------- | ---------------------- |
| POST | `/api/v2/enterprise/{enterpriseId}/safes/generate` | `Safes.generateSafe` |
| POST | `/api/v2/enterprise/{enterpriseId}/safes/{safeId}/wallets/generate` | `Safe.createWallet` |
| POST | `/api/v2/enterprise/{enterpriseId}/safes/{safeId}/keys/generate` | `Safes.createSafeKeys` |

`POST /api/v2/enterprise/{enterpriseId}/safes` (without `/generate`) is initialize-only and is proxied to the BitGo API. Listing, get, finalize, archive, freeze, and update are also proxied.

Default Express timeout is 305 seconds. `generateSafe` can run up to four parallel root-key ceremonies; raise `--timeout` if a run is cut off.

When signing a safe child wallet through existing Express send/sign routes, `WALLET_{walletId}_PASSPHRASE` is the **safe** passphrase (it decrypts the root user keychain).

# Release Notes

You can find the complete release notes (since version 4.44.0) [here](https://github.com/BitGo/BitGoJS/blob/master/CHANGELOG.md).
3 changes: 2 additions & 1 deletion modules/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@api-ts/typed-express-router": "2.0.0",
"@bitgo/abstract-lightning": "^8.3.9",
"@bitgo/logger": "^1.4.0",
"@bitgo/public-types": "6.79.0",
"@bitgo/sdk-core": "^38.19.0",
"@bitgo/utxo-lib": "^11.24.4",
"@types/proxyquire": "^1.3.31",
Expand All @@ -60,7 +61,7 @@
"superagent": "^9.0.1"
},
"devDependencies": {
"@bitgo/public-types": "6.79.0",

"@bitgo/sdk-lib-mpc": "^11.0.0",
"@bitgo/sdk-test": "^9.1.80",
"@types/argparse": "^1.0.36",
Expand Down
53 changes: 53 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
IncorrectPasswordError,
MPCType,
multisigTypes,
Safes,
ShareType,
SignShare,
SShare,
Expand Down Expand Up @@ -704,6 +705,50 @@ export async function handleV2GenerateWallet(req: ExpressApiRouteRequest<'expres
return { ...result, wallet: result.wallet.toJSON() };
}

/**
* One-shot Wallet Safe creation: initialize → local root-key ceremonies → finalize.
*/
export async function handleV2GenerateSafe(
req: ExpressApiRouteRequest<'express.v2.safes.generate', 'post'>
): Promise<any> {
const safes = new Safes(req.bitgo, req.decoded.enterpriseId);
const safe = await safes.generateSafe({
label: req.decoded.label,
passphrase: req.decoded.passphrase,
});
return safe.toJSON();
}

/**
* Mint a hot child wallet from a Wallet Safe (onchain hardened derive or TSS ceremony).
*/
export async function handleV2GenerateSafeWallet(
req: ExpressApiRouteRequest<'express.v2.safes.wallet.generate', 'post'>
) {
const safes = new Safes(req.bitgo, req.decoded.enterpriseId);
const safe = await safes.get({ id: req.decoded.safeId });
const wallet = await safe.createWallet({
coin: req.decoded.coin,
label: req.decoded.label,
passphrase: req.decoded.passphrase,
multisigType: req.decoded.multisigType,
});
return wallet.toJSON();
}

/**
* Phase-2-only Safe key ceremonies for an already-initialized safe.
*/
export async function handleV2GenerateSafeKeys(req: ExpressApiRouteRequest<'express.v2.safes.keys.generate', 'post'>) {
const safes = new Safes(req.bitgo, req.decoded.enterpriseId);
return safes.createSafeKeys({
label: '',
passphrase: req.decoded.passphrase,
safeId: req.decoded.safeId,
enabledRootSlots: req.decoded.enabledRootSlots,
});
}

/**
* Independently verify newly created address(es) against client-supplied trusted keychains.
*
Expand Down Expand Up @@ -2103,6 +2148,14 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
// generate wallet
router.post('express.wallet.generate', [prepareBitGo(config), typedPromiseWrapper(handleV2GenerateWallet)]);

// generate wallet safe / mint a child wallet from a safe / phase-2 key ceremonies
router.post('express.v2.safes.generate', [prepareBitGo(config), typedPromiseWrapper(handleV2GenerateSafe)]);
router.post('express.v2.safes.wallet.generate', [
prepareBitGo(config),
typedPromiseWrapper(handleV2GenerateSafeWallet),
]);
router.post('express.v2.safes.keys.generate', [prepareBitGo(config), typedPromiseWrapper(handleV2GenerateSafeKeys)]);

router.put('express.wallet.update', [prepareBitGo(config), typedPromiseWrapper(handleWalletUpdate)]);

// change wallet passphrase
Expand Down
19 changes: 18 additions & 1 deletion modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import { PutFanoutUnspents } from './v1/fanoutUnspents';
import { PostOfcSignPayload } from './v2/ofcSignPayload';
import { PostWalletRecoverToken } from './v2/walletRecoverToken';
import { PostGenerateWallet } from './v2/generateWallet';
import { PostGenerateSafe } from './v2/generateSafe';
import { PostGenerateSafeWallet } from './v2/generateSafeWallet';
import { PostGenerateSafeKeys } from './v2/generateSafeKeys';
import { PostSignerMacaroon } from './v2/signerMacaroon';
import { PostCoinSignTx } from './v2/coinSignTx';
import { PostWalletSignTx } from './v2/walletSignTx';
Expand Down Expand Up @@ -356,6 +359,18 @@ export const ExpressWalletManagementApiSpec = apiSpec({
},
});

export const ExpressSafesApiSpec = apiSpec({
'express.v2.safes.generate': {
post: PostGenerateSafe,
},
'express.v2.safes.wallet.generate': {
post: PostGenerateSafeWallet,
},
'express.v2.safes.keys.generate': {
post: PostGenerateSafeKeys,
},
});

export const ExpressV2CanonicalAddressApiSpec = apiSpec({
'express.canonicaladdress': {
post: PostCanonicalAddress,
Expand Down Expand Up @@ -429,7 +444,8 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressV2WalletResourceDelegationsApiSpec &
typeof ExpressV2WalletDelegateResourcesApiSpec &
typeof ExpressV2WalletUndelegateResourcesApiSpec &
typeof ExpressWalletManagementApiSpec;
typeof ExpressWalletManagementApiSpec &
typeof ExpressSafesApiSpec;

export const ExpressApi: ExpressApi = {
...ExpressPingApiSpec,
Expand Down Expand Up @@ -476,6 +492,7 @@ export const ExpressApi: ExpressApi = {
...ExpressV2WalletDelegateResourcesApiSpec,
...ExpressV2WalletUndelegateResourcesApiSpec,
...ExpressWalletManagementApiSpec,
...ExpressSafesApiSpec,
};

type ExtractDecoded<T> = T extends t.Type<any, infer O, any> ? O : never;
Expand Down
64 changes: 64 additions & 0 deletions modules/express/src/typedRoutes/api/v2/generateSafe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for one-shot Safe generation.
*/
export const GenerateSafeParams = {
/** Enterprise public id that owns the safe */
enterpriseId: t.string,
} as const;

/**
* Request body for one-shot Safe generation.
*
* `enabledRootSlots` is decided by the server at initialize (Flipt); do not send it here.
*/
export const GenerateSafeBody = {
/** Safe label */
label: t.string,
/** Passphrase used to encrypt locally generated root user/backup keys and to run MPC ceremonies */
passphrase: t.string,
} as const;

/**
* Response body for one-shot Safe generation.
*/
export const GenerateSafeResponse = {
/** The newly created, finalized safe */
200: t.UnknownRecord,
/** Bad request */
400: BitgoExpressError,
} as const;

/**
* Generate Safe
*
* One-shot Wallet Safe creation for hot custody. Runs locally on BitGo Express:
*
* 1. Initialize the safe on BitGo (metadata only).
* 2. Run the enabled root-key ceremonies on this machine (multisig keygen + MPC), encrypted with `passphrase`.
* 3. Finalize the safe with the minted root key ids.
*
* If a ceremony fails, the SDK archives the half-created safe and throws with the per-slot failure list.
* Create a new safe and retry.
*
* ⓘ This endpoint must be called through BitGo Express. `POST /api/v2/enterprise/{enterpriseId}/safes`
* (without `/generate`) is the server-side initialize-only call and does not run local crypto.
*
* ⓘ Production Express requires TLS. The passphrase never leaves the Express host.
*
* @operationId express.v2.safes.generate
* @tag Express
* @private
*/
export const PostGenerateSafe = httpRoute({
path: '/api/v2/enterprise/{enterpriseId}/safes/generate',
method: 'POST',
request: httpRequest({
params: GenerateSafeParams,
body: GenerateSafeBody,
}),
response: GenerateSafeResponse,
});
63 changes: 63 additions & 0 deletions modules/express/src/typedRoutes/api/v2/generateSafeKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { RootKeyType } from '@bitgo/public-types';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for Phase-2-only Safe key ceremonies.
*/
export const GenerateSafeKeysParams = {
/** Enterprise public id that owns the safe */
enterpriseId: t.string,
/** Already-initialized safe id to tag the ceremonies with */
safeId: t.string,
} as const;

/**
* Request body for Phase-2-only Safe key ceremonies.
*
* `initializeSafe` and `finalizeSafe` remain direct server calls (no local crypto).
*/
export const GenerateSafeKeysBody = {
/** Passphrase used to encrypt locally generated root user/backup keys and to run MPC ceremonies */
passphrase: t.string,
/** Slots to run. Omit to run all four (older WP / retry of a full ceremony set). */
enabledRootSlots: optional(t.array(RootKeyType)),
} as const;

/**
* Response body for Phase-2-only Safe key ceremonies.
*/
export const GenerateSafeKeysResponse = {
/** Minted root key ids, as ordered [user, backup, bitgo] triplets — the payload `finalizeSafe` consumes */
200: t.UnknownRecord,
/** Bad request */
400: BitgoExpressError,
} as const;

/**
* Generate Safe Keys
*
* Phase 2 of Wallet Safe creation: run the enabled root-key ceremonies for an already-initialized
* safe. Use this when a thin client already called `POST /safes` (initialize) and only needs the
* local ceremonies before `POST .../finalize`.
*
* If a ceremony fails, the SDK archives the half-created safe and throws with the per-slot failure
* list. Create a new safe and retry.
*
* ⓘ This endpoint must be called through BitGo Express. One-shot creation is
* `POST /api/v2/enterprise/{enterpriseId}/safes/generate`.
*
* @operationId express.v2.safes.keys.generate
* @tag Express
* @private
*/
export const PostGenerateSafeKeys = httpRoute({
path: '/api/v2/enterprise/{enterpriseId}/safes/{safeId}/keys/generate',
method: 'POST',
request: httpRequest({
params: GenerateSafeKeysParams,
body: GenerateSafeKeysBody,
}),
response: GenerateSafeKeysResponse,
});
68 changes: 68 additions & 0 deletions modules/express/src/typedRoutes/api/v2/generateSafeWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { WalletResponse, multisigType } from '../../schemas/wallet';

/**
* Path parameters for minting a child wallet from a Safe.
*/
export const GenerateSafeWalletParams = {
/** Enterprise public id that owns the safe */
enterpriseId: t.string,
/** Safe id to mint the wallet from */
safeId: t.string,
} as const;

/**
* Request body for minting a child wallet from a Safe.
*
* Safe wallets are hot-only in v1. Child keys are derived locally; do not send `keys`.
*/
export const GenerateSafeWalletBody = {
/** Coin ticker / chain identifier for the child wallet */
coin: t.string,
/** Wallet label */
label: t.string,
/** Safe passphrase — decrypts the root user keychain and encrypts derived material */
passphrase: t.string,
/** `onchain` (default) mints a secp256k1/ed25519 multisig wallet; `tss` mints an MPC wallet */
multisigType: optional(multisigType),
} as const;

/**
* Response body for minting a child wallet from a Safe.
*/
export const GenerateSafeWalletResponse = {
/** The minted child wallet */
200: WalletResponse,
/** Bad request */
400: BitgoExpressError,
} as const;

/**
* Generate Safe Wallet
*
* Mint a hot child wallet from an existing Wallet Safe. Runs locally on BitGo Express:
*
* - `onchain`: decrypt the safe root, hardened-derive the user child, register it, then mint.
* - `tss`: decrypt the MPC root blob, run the user/BitGo hard-derive ceremony, then mint.
*
* ⓘ This endpoint must be called through BitGo Express. `POST /api/v2/enterprise/{enterpriseId}/safes/{safeId}/wallets`
* (without `/generate`) is the server-side mint that expects pre-built `keys`.
*
* ⓘ For later signing through Express, `WALLET_{walletId}_PASSPHRASE` is the **safe** passphrase
* (it decrypts the root user keychain, not a child envelope).
*
* @operationId express.v2.safes.wallet.generate
* @tag Express
* @private
*/
export const PostGenerateSafeWallet = httpRoute({
path: '/api/v2/enterprise/{enterpriseId}/safes/{safeId}/wallets/generate',
method: 'POST',
request: httpRequest({
params: GenerateSafeWalletParams,
body: GenerateSafeWalletBody,
}),
response: GenerateSafeWalletResponse,
});
Loading
Loading