Skip to content

Commit

Permalink
feat(MPC-2011): add mTLS client cert auth support
Browse files Browse the repository at this point in the history
  • Loading branch information
MnrGreg committed Nov 14, 2024
1 parent 87442a9 commit 0d341ac
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 99 deletions.
23 changes: 19 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
BLOCKDAEMON_RPC_URL="https://svc.blockdaemon.com/native/v1/ethereum/holesky?apiKey=zpka_..."
BUILDERVAULT_PLAYER_COUNT=3 # Count of BuilderVault MPC Node Players
BUILDERVAULT_PLAYER0_URL="http://localhost:8500"
BUILDERVAULT_PLAYER0_APIKEY="..."
BUILDERVAULT_PLAYER0_MPCPUBLICKEY="MFkwEwYHKoZIzj0....gO+224X8T0J9eMg=" # Base64 encoding of MPC Player public key. Used in Dynamic communications with broker setups
BUILDERVAULT_PLAYER1_URL="http://localhost:8501"
BUILDERVAULT_PLAYER1_APIKEY="..."
BUILDERVAULT_MASTERKEY_ID="Ap7..."
BUILDERVAULT_PLAYER1_MPCPUBLICKEY="MFkwEwYHKoZIzj0....gO+224X8T0J9eMg=" # Base64 encoding of MPC Player public key. Used in Dynamic communications with broker setups

BUILDERVAULT_MASTERKEY_ID="Ap7..."
BUILDERVAULT_ACCOUNT_ID=0
BUILDERVAULT_ADDRESS_INDEX=0
BUILDERVAULT_ADDRESS_INDEX=0

BUILDERVAULT_PLAYER0_APIKEY="..."
BUILDERVAULT_PLAYER1_APIKEY="..."

## mTLS-based client certificate authentication key pair paths
# BUILDERVAULT_PLAYER0_CLIENT_CERT="./client.crt"
# BUILDERVAULT_PLAYER0_CLIENT_KEY="./client.key"
# BUILDERVAULT_PLAYER1_CLIENT_CERT="./client.crt"
# BUILDERVAULT_PLAYER1_CLIENT_KEY="./client.key"

## Optional mTLS-based server certificate pinning
# BUILDERVAULT_PLAYER0_MTLSPUBLICKEY="-----BEGIN CERTIFICATE-----\nMIICMTCCAdegAwIBAg...iABMV+KTXJxA==\n-----END CERTIFICATE-----"
# BUILDERVAULT_PLAYER1_MTLSPUBLICKEY="-----BEGIN CERTIFICATE-----\nMIICMjCCAdegAwIBAg...srtGsDhLOe8O8=\n-----END CERTIFICATE-----"
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,37 @@ const walletClient = createWalletClient({
### BuildervaultProviderConfig

```ts
type BuildervaultProviderConfig = {
export type BuildervaultProviderConfig = {
// ------------- Mandatory fields -------------
/**
* Set the RPC API URL endpoint for JSON-RPC over HTTP access to the blockchain data
*/
rpcUrl?: string,
/**
* Set the URL of the BuilderVault player0 and player1 endpoints
* Set the numnber of the BuilderVault players
*/
playerCount?: number
/**
* Set the URL of each BuilderVault player endpoint
*/
player0Url?: string,
player1Url?: string,
/**
* Set the BuilderVault TSM API keys
player2Url?: string,
/**
* Set the BuilderVault TSM API keys or use Client Certificate authentication
*/
player0ApiKey?: string,
player1ApiKey?: string,
player2ApiKey?: string,
/**
* Set the BuilderVault TSM mTLS Client Authentication Certficate key pair or use API Key authentication
*/
player0ClientCert?: string,
player0ClientKey?: string,
player1ClientCert?: string,
player1ClientKey?: string,
player2ClientCert?: string,
player2ClientKey?: string,
/**
* BuilderVault Master Key ID. This ID represents all the private Master Key shares and must be generated outside if the web3 provider using the BuilderVault SDK
*/
Expand All @@ -88,12 +103,25 @@ type BuildervaultProviderConfig = {
addressIndex?: number,

// ------------- Optional fields --------------
/**
* Set the MPC publickey of each BuilderVault player. This is required for Dynamic communication between nodes such as through a broker and not static communication
*/
player0MPCpublicKey?: string,
player1MPCpublicKey?: string,
player2MPCpublicKey?: string,
/**
/**
* Set the TLS publickey of each BuilderVault player endpoint. This is used for mTLS server certificate pinning.
*/
player0mTLSpublicKey?: string,
player1mTLSpublicKey?: string,
player2mTLSpublicKey?: string,
/**
* Default: false
* By setting to true, every request and response processed by the provider will be logged to the console
* Same as setting env var `DEBUG=buildervault-web3-provider:req_res`
*/
logRequestsAndResponses?: boolean
logRequestsAndResponses?: boolean,
}
```
Expand Down
139 changes: 75 additions & 64 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import asn1 from "asn1.js";
import { keccak256, toHex, hexToBytes, toChecksumAddress, hexToString, hexToNumber } from 'web3-utils';
import { encodeParameters } from 'web3-eth-abi';
import { FeeMarketEIP1559Transaction, hashMessage } from 'web3-eth-accounts';
import crypto from "crypto";
import util from "util";
import { promiseToFunction } from "./utils";
import { AccountAddresses, BuildervaultProviderConfig, EthereumSignature, ProviderRpcError, RequestArguments } from "./types";
Expand All @@ -16,12 +17,13 @@ const logRequestsAndResponses = Debug(DEBUG_NAMESPACE_REQUESTS_AND_RESPONSES);

export class BuildervaultWeb3Provider extends HttpProvider {
private config: BuildervaultProviderConfig;
private headers: { name: string, value: string }[] = [];
private accountsAddresses: AccountAddresses = {};
private accountId: number;
private accountsPopulatedPromise: () => Promise<void>;
private chainIdPopulatedPromise: () => Promise<void>;
private requestCounter = 0;
private TSMClients: TSMClient[] = [];
private sessionConfig: SessionConfig = [];

constructor(config: BuildervaultProviderConfig) {
if (!config.rpcUrl) {
Expand All @@ -34,8 +36,6 @@ export class BuildervaultWeb3Provider extends HttpProvider {
}
Debug.enable(debugNamespaces.join(','))

const headers: { name: string, value: string }[] = []

super(config.rpcUrl)
this.config = config

Expand All @@ -45,14 +45,72 @@ export class BuildervaultWeb3Provider extends HttpProvider {
this.chainPath = new Uint32Array([44, 60, this.accountId, 0, this.addressIndex]);
this.accountsAddresses[this.accountId] = {};

this.headers = headers;

this.note = 'Created by BuilderVault Web3 Provider'

this.chainIdPopulatedPromise = promiseToFunction(async () => { if (!this.chainId) return await this.populateChainId() })
this.accountsPopulatedPromise = promiseToFunction(async () => { return await this.populateAccounts() })
}

private async initializeTSMClients() {

// For each player create an authenticated TSMClient
if (this.config.playerCount) {
for (let i = 0; i < this.config.playerCount; i++) {
const playerUrlConfigKey = `player${i}Url`;
const playerApiKeyConfigKey = `player${i}ApiKey`;
const playerClientCertConfigKey = `player${i}ClientCert`;
const playerClientKeyConfigKey = `player${i}ClientKey`;
const playerMTLSpublicKeyConfigKey = `player${i}mTLSpublicKey`;

if (playerUrlConfigKey in this.config) {
const playerConfig = await new Configuration((this.config as { [key: string]: any })[playerUrlConfigKey]);

if (playerApiKeyConfigKey in this.config) {
await playerConfig.withAPIKeyAuthentication((this.config as { [key: string]: any })[playerApiKeyConfigKey]);
} else if (playerClientCertConfigKey in this.config && playerClientKeyConfigKey in this.config && playerMTLSpublicKeyConfigKey in this.config) {
const cert = new crypto.X509Certificate((this.config as { [key: string]: any })[playerMTLSpublicKeyConfigKey]);
await playerConfig.withMTLSAuthentication(
(this.config as { [key: string]: any })[playerClientKeyConfigKey],
(this.config as { [key: string]: any })[playerClientCertConfigKey],
cert.publicKey.export({ type: "spki", format: "der" })
);
} else {
throw new Error(`player${i} authentication credentials are required`);
}

this.TSMClients.push(await TSMClient.withConfiguration(playerConfig));
} else {
throw new Error(`${playerUrlConfigKey} not found`);
}

}

// If player MPC publickeys are defined construct new Dynamic SessionConfig
if (this.config.player0MPCpublicKey) {
const playerPubkeys = [];
const playerIds = new Uint32Array(Array(this.TSMClients.length).fill(0).map((_, i) => i));
for (let i = 0; i < this.config.playerCount; i++) {
const playerMPCpublicKeyConfigKey = Buffer.from(
(this.config as { [key: string]: any })[`player${i}MPCpublicKey`], "base64"
)
playerPubkeys.push(playerMPCpublicKeyConfigKey)
}
this.sessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
playerIds,
playerPubkeys
);
// If player MPC publickeys are not defined construct new Static SessionConfig
} else {
this.sessionConfig = await SessionConfig.newStaticSessionConfig(
await SessionConfig.GenerateSessionID(),
this.TSMClients.length
);
}

} else {
throw new Error('playerCount is required');
}

}

private async populateChainId() {
const chainId = (await util.promisify<any, any>(super.send).bind(this)(formatJsonRpcRequest('eth_chainId', []))).result
Expand All @@ -64,35 +122,15 @@ export class BuildervaultWeb3Provider extends HttpProvider {
if (this.accountsAddresses[0]?.[0] !== undefined) {
throw this.createError({ message: "Accounts already populated" })
}

let player0config
if (this.config.player0ApiKey) {
player0config = await new Configuration(this.config.player0Url);
await player0config.withAPIKeyAuthentication(this.config.player0ApiKey);
} else {
throw new Error('player0ApiKey is required');
}

let player1config
if (this.config.player1ApiKey) {
player1config = await new Configuration(this.config.player1Url);
await player1config.withAPIKeyAuthentication(this.config.player1ApiKey);
} else {
throw new Error('player1ApiKey is required');
}

const TSMClients: TSMClient[] = [
await TSMClient.withConfiguration(player0config),
await TSMClient.withConfiguration(player1config)
];
await this.initializeTSMClients();

// ToDo: include this.addressIndex in loop when outside 0-5
for (let i = 0; i < 5; i++) {

let chainPath = new Uint32Array([44, 60, this.accountId, 0, i]);
const pkixPublicKeys: Uint8Array[] = [];

for (const [_, client] of TSMClients.entries()) {
for (const [_, client] of await this.TSMClients.entries()) {
const ecdsaApi = client.ECDSA();
pkixPublicKeys.push(
await ecdsaApi.publicKey(this.masterKeyId, chainPath)
Expand All @@ -109,7 +147,7 @@ export class BuildervaultWeb3Provider extends HttpProvider {
const pkixPublicKey = pkixPublicKeys[0];

// Convert the public key into an Ethereum address
const utils = TSMClients[0].Utils();
const utils = this.TSMClients[0].Utils();
const publicKeyBytes = await utils.pkixPublicKeyToUncompressedPoint(
pkixPublicKey
);
Expand Down Expand Up @@ -300,7 +338,7 @@ export class BuildervaultWeb3Provider extends HttpProvider {

const {r,s,v} = await this.signTx(unsignedTxHash, this.masterKeyId, this.chainPath);

const signedTransaction = unsignedTx._processSignature(v.valueOf(), hexToBytes(r), hexToBytes(s));
const signedTransaction = unsignedTx._processSignature(BigInt(v), hexToBytes(r), hexToBytes(s));

const serializeTx = FeeMarketEIP1559Transaction.fromTxData(signedTransaction).serialize();
console.log('Broadcasting signed transaction:', toHex(serializeTx));
Expand Down Expand Up @@ -447,45 +485,18 @@ export class BuildervaultWeb3Provider extends HttpProvider {
chainPath: Uint32Array
): Promise<EthereumSignature> {


console.log(`Builder Vault signing transaction hash...`);

let player0config
if (this.config.player0ApiKey) {
player0config = await new Configuration(this.config.player0Url);
await player0config.withAPIKeyAuthentication(this.config.player0ApiKey);
} else {
throw new Error('player0ApiKey is required');
}

let player1config
if (this.config.player1ApiKey) {
player1config = await new Configuration(this.config.player1Url);
await player1config.withAPIKeyAuthentication(this.config.player1ApiKey);
} else {
throw new Error('player1ApiKey is required');
}

const clients: TSMClient[] = [
await TSMClient.withConfiguration(player0config),
await TSMClient.withConfiguration(player1config)
];

const sessionConfig = await SessionConfig.newStaticSessionConfig(
await SessionConfig.GenerateSessionID(),
clients.length
);


const partialSignatures: Uint8Array[] = [];

const partialSignaturePromises: Promise<void>[] = [];

for (const [_, client] of clients.entries()) {
for (const [_, client] of this.TSMClients.entries()) {
const func = async (): Promise<void> => {
const ecdsaApi = client.ECDSA();
console.log(`Creating partialSignature with MPC player ${_}...`);
const partialSignResult = await ecdsaApi.sign(
sessionConfig,
this.sessionConfig,
masterKeyId,
chainPath,
messageToSign
Expand All @@ -499,7 +510,7 @@ export class BuildervaultWeb3Provider extends HttpProvider {

await Promise.all(partialSignaturePromises);

const ecdsaApi = clients[0].ECDSA();
const ecdsaApi = this.TSMClients[0].ECDSA();

const signature = await ecdsaApi.finalizeSignature(
messageToSign,
Expand All @@ -519,8 +530,8 @@ export class BuildervaultWeb3Provider extends HttpProvider {
return {
r: "0x" + decodedSignature.r.toString(16),
s: "0x" + decodedSignature.s.toString(16),
v: BigInt(signature.recoveryID! + 27), // Type 2 transaction with ._processSignature subtracts 27 Post EIP-155 should be: chainId * 2 + 35 + signature.recoveryID;
v: signature.recoveryID+27 // Type 2 transaction with ._processSignature subtracts 27 Post EIP-155 should be: chainId * 2 + 35 + signature.recoveryID;
};
}

}
}
Loading

0 comments on commit 0d341ac

Please sign in to comment.