Skip to content
Draft
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
39 changes: 39 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,41 @@ export async function handleV2IsWalletAddress(
return await wallet.baseCoin.isWalletAddress(req.decoded as any);
}

/**
* handle v2 verifyPrivateKey - verify that a private key matches a wallet's user keychain
* @param req
*/
export async function handleV2VerifyPrivateKey(
req: ExpressApiRouteRequest<'express.v2.wallet.verifyPrivateKey', 'post'>
): Promise<{ valid: boolean }> {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.decoded.coin);
const { prv, encryptedPrv, walletPassphrase, multiSigType, publicKey: explicitPublicKey } = req.decoded;

if (!prv && !encryptedPrv) {
throw new ApiResponseError('either prv or encryptedPrv is required', 400);
}
if (encryptedPrv && !walletPassphrase) {
throw new ApiResponseError('walletPassphrase is required when encryptedPrv is provided', 400);
}

let resolvedPublicKey = explicitPublicKey;
if (!resolvedPublicKey) {
const wallet = await coin.wallets().get({ id: req.decoded.id });
const keychains = await coin.keychains().getKeysForSigning({ wallet });
const userKeychain = keychains[0];
resolvedPublicKey = userKeychain.commonKeychain ?? userKeychain.pub;
}

const auditParams =
prv !== undefined
? { prv, multiSigType, publicKey: resolvedPublicKey }
: { encryptedPrv: encryptedPrv!, walletPassphrase: walletPassphrase!, multiSigType, publicKey: resolvedPublicKey };

await coin.assertIsValidKey(auditParams as any);
return { valid: true };
}

/**
* handle v2 deriveAddress - locally derive and return a wallet receive address from a
* derivation path, using public key material only.
Expand Down Expand Up @@ -2057,6 +2092,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
prepareBitGo(config),
typedPromiseWrapper(handleV2IsWalletAddress),
]);
router.post('express.v2.wallet.verifyPrivateKey', [
prepareBitGo(config),
typedPromiseWrapper(handleV2VerifyPrivateKey),
]);
router.post('express.v2.address.derive', [prepareBitGo(config), typedPromiseWrapper(handleV2DeriveAddress)]);

router.post('express.wallet.share', [prepareBitGo(config), typedPromiseWrapper(handleV2ShareWallet)]);
Expand Down
9 changes: 9 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { PostWalletEnableTokens } from './v2/walletEnableTokens';
import { PostWalletSweep } from './v2/walletSweep';
import { PostWalletAccelerateTx } from './v2/walletAccelerateTx';
import { PostIsWalletAddress } from './v2/isWalletAddress';
import { PostVerifyPrivateKey } from './v2/verifyPrivateKey';
import { PostDeriveAddress } from './v2/deriveAddress';
import { GetAccountResources } from './v2/accountResources';
import { GetResourceDelegations } from './v2/resourceDelegations';
Expand Down Expand Up @@ -236,6 +237,12 @@ export const ExpressV2WalletIsWalletAddressApiSpec = apiSpec({
},
});

export const ExpressV2WalletVerifyPrivateKeyApiSpec = apiSpec({
'express.v2.wallet.verifyPrivateKey': {
post: PostVerifyPrivateKey,
},
});

export const ExpressV2AddressDeriveApiSpec = apiSpec({
'express.v2.address.derive': {
post: PostDeriveAddress,
Expand Down Expand Up @@ -406,6 +413,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressWalletFanoutUnspentsApiSpec &
typeof ExpressV2WalletCreateAddressApiSpec &
typeof ExpressV2WalletIsWalletAddressApiSpec &
typeof ExpressV2WalletVerifyPrivateKeyApiSpec &
typeof ExpressV2AddressDeriveApiSpec &
typeof ExpressKeychainLocalApiSpec &
typeof ExpressKeychainChangePasswordApiSpec &
Expand Down Expand Up @@ -452,6 +460,7 @@ export const ExpressApi: ExpressApi = {
...ExpressV2WalletCreateAddressApiSpec,
...ExpressV2WalletConsolidateAccountApiSpec,
...ExpressV2WalletIsWalletAddressApiSpec,
...ExpressV2WalletVerifyPrivateKeyApiSpec,
...ExpressV2AddressDeriveApiSpec,
...ExpressKeychainLocalApiSpec,
...ExpressKeychainChangePasswordApiSpec,
Expand Down
91 changes: 91 additions & 0 deletions modules/express/src/typedRoutes/api/v2/verifyPrivateKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional, type HttpRoute } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for verifying a private key against a wallet
*/
export const VerifyPrivateKeyParams = {
/** A cryptocurrency or token ticker symbol. */
coin: t.string,
/** The wallet ID */
id: t.string,
} as const;

/**
* Request body for verifying that a private key matches a wallet's user keychain.
* Exactly one of prv or encryptedPrv must be provided.
*/
export const VerifyPrivateKeyBody = {
/**
* The plaintext private key to verify.
* Mutually exclusive with encryptedPrv + walletPassphrase.
*/
prv: optional(t.string),

/**
* Encrypted private key (BitGo keycard format).
* Must be supplied together with walletPassphrase.
*/
encryptedPrv: optional(t.string),

/**
* Passphrase used to decrypt encryptedPrv.
* Required when encryptedPrv is provided.
*/
walletPassphrase: optional(t.string),

/**
* The multisig type of the wallet.
* Use 'tss' for TSS/MPC wallets; omit or use 'onchain' for standard wallets.
*/
multiSigType: optional(t.union([t.literal('tss'), t.literal('onchain')])),

/**
* The public key corresponding to the private key.
* Required for TSS wallets where it is the common keychain.
* For standard wallets, if omitted the user keychain pub is fetched from the wallet.
*/
publicKey: optional(t.string),
} as const;

/**
* Successful verification response
*/
export const VerifyPrivateKeyResponse200 = t.type({
/** Whether the private key is valid for the wallet's user keychain */
valid: t.boolean,
});

/**
* Response for verifying a private key against a wallet
*/
export const VerifyPrivateKeyResponse = {
200: VerifyPrivateKeyResponse200,
400: BitgoExpressError,
} as const;

/**
* Verify that a private key matches the user keychain of a wallet.
*
* This endpoint accepts either a plaintext private key (prv) or an encrypted
* private key (encryptedPrv + walletPassphrase). It fetches the wallet's user
* keychain from BitGo to obtain the corresponding public key, then calls
* coin.assertIsValidKey to cryptographically verify the private key.
*
* Returns { valid: true } when the private key is valid for the wallet.
* Returns a 400 error if required parameters are missing or invalid.
* Throws if the private key does not match the public key on record.
*
* @operationId express.v2.wallet.verifyPrivateKey
* @tag Express
*/
export const PostVerifyPrivateKey: HttpRoute<'post'> = httpRoute({
path: '/api/v2/{coin}/wallet/{id}/verifyPrivateKey',
method: 'POST',
request: httpRequest({
params: VerifyPrivateKeyParams,
body: VerifyPrivateKeyBody,
}),
response: VerifyPrivateKeyResponse,
});
Loading
Loading