Key Generation and Signing
Once the SDK is authenticated, it can be used to do operations on the TSM, such as generating keys and signing messages using the generated keys.
MPC Sessions
To generate a key in the TSM, the MPC nodes must be instructed to perform an MPC session. During the MPC session, the MPC nodes will interact with each other, often in several rounds, according to a specific MPC protocol.
To start an MPC session, all MPC nodes that participate in the session must agree on
- A unique MPC session ID
- The subset of MPC nodes that should participate in the MPC session
It is up to you to generate the session ID and choose the particular subset of MPC nodes for the MPC session, and you must make sure that this information is available to the SDK of each of the MPC nodes that should participate in the session.
The session ID must be a unique string that fits in the header of an HTTP request. You can use a helper method in the SDK to generate a session ID:
sessionID := tsm.GenerateSessionID()
Each MPC node in the TSM is identified by a unique integer (often numbered 0, 1, 2, 3, and so forth). So if your MPC nodes are numbered 0, 1, 2, 3, then you may, for example, choose the subset (1, 2, 3) for the particular MPC session.
players := []int{1,2,3}
The next step is to request the MPC session on each of the SDKs of the MPC nodes that participate in the MPC session. This is done by first creating a SessionConfig
object containing the session ID and the player subset. Then, you call a specific method on the SDK, providing the session configuration as an input parameter.
Key Generation
In our case, we will use the session configuration to run an MPC session that generates a new ECDSA key over the secp256k1 curve. This is done by calling the GenerateKey()
method on the SDK, with the session configuration.
In the call to GenerateKey()
an additional parameter threshold must also be provided. This is the security threshold for the key to be generated. The MPC session will generate the key as a secret sharing among the MPC nodes in the TSM, and a security threshold of t means that a secret sharing will be generated that keeps the key secret even if an attacker manages to steal up to t of the key shares.
To summarize, the MPC key generation session between MPC Node 1, 2, and 3 is started by invoking this on the SDKs controlling the three MPC nodes:
threshold := 1 // The security threshold we want for this key
context := context.Background()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
curveName := "secp256k1"
keyID, err := client.ECDSA().GenerateKey(context, sessionConfig, threshold, curveName, "")
When GenerateKey()
is called on the SDK, and it forwards the key generation request to its MPC node.
Importantly, the actual MPC session only starts when all the MPC nodes in the MPC session have received a key generation request from their respective SDKs. When an MPC node receives a request for an MPC session, it sends a message to the other MPC nodes in the session, and it only proceeds with the MPC session when it has received a message from all other MPC nodes in the session.
As a consequence, if only some of the MPC nodes in the session request the MPC session via their SDK, the MPC nodes will wait and time out after a while, returning an error message to the SDK. The default timeout is 10 seconds, which can be configured in the MPC node configuration files.
As a part of the MPC session, the MPC nodes will check that they agree on the threshold
and the curveName
. So, in addition to agreeing on the session ID and the player subset, the SDK operators must also make sure they use the same values for these parameters before calling the SDK.
If the MPC session is successful, the key will be generated as a secret shared among the MPC nodes, and each SDK will receive the new key's key ID.
Other Signature Schemes and Curves
This example uses ECDSA over the secp256k1 curve. Builder Vault supports several other signature schemes and curves. You can see a list of supported schemes here. If you for example want ECDSA over the curve P-521 instead of secp256k1, you can simply use "P-521" instead of "secp256k1" as
curveName
in the example.Schnorr signatures such as Ed25519, you just need to use
client.Schnorr()
instead ofclient.ECDSA()
. An example of this can be seen here
Obtaining the Public Key
After key generation, the public key can be obtained like this:
publicKey, err = client.ECDSA().PublicKey(ctx, keyID, nil)
The public key is returned in a JSON format
{
"scheme": "...",
"curve": "...",
"point": "..."
}
where scheme
is either ECDSA
or the name of one of the Schnorr schemes. The curve
is the name of the elliptic curve, but can be empty if it is uniquely defined by the scheme. Finally, point
is a compressed or uncompressed point representing the public key. You can convert the returned JSON poublic key to a SubjectPublicKeyInfo structure (see RFC 5280, Section 4.1) as follows:
pkixPublicKey, err := tsmutils.ConvertJSONPublicKeyToPKIXPublicKey(publicKey)
This only works for ECDSA, Ed25519 and Ed448 keys.
When to Trust the Public Key
The key generation protocol ensures that honest MPC nodes that complete the protocol without an abort will hold a correct share of the private key as well as a copy of the correct public key in its database. But the protocol does not guarantee that all honest MPC nodes will complete the protocol without an abort. In the worst case, a malicious MPC node may cause all or most of the honest MPC nodes to abort before the key generation session finishes, meaning that they will not get to store their key shares in their databases.
The
PublicKey()
method simply fetches the public key from a single MPC node. So it is important to use the public key, e.g., for creating a wallet address that is handed to users, only after you have received a message from all MPC nodes stating that they have succeeded the key generation session without an abort. Otherwise there may not be enough MPC nodes holding key shares of the private key in order to generate signatures. A good way to do this is to collect the public key from all MPC nodes in your application and then check that they are all equal.
Signing
Once a key has been generated, each SDK now holds a key ID, and it is ready to sign messages using the key.
Like key generation, signatures are generated by running an MPC session. Once again, you must generate a session ID and choose a subset of MPC nodes. The nodes should be chosen among the nodes that participated in the key generation. You should also pick the message hash to be signed, for example:
sessionID := tsm.GenerateSessionID()
players := []int{1,2}
message := []byte("This message could be a transaction to be signed")
msgHash := sha256.Sum256(message)
You must now propagate this information to each of the SDK operators, in this example, the operators of the SDK for Node 1 and Node 2. They must each request the MPC session on their respective SDK using the key ID obtained from the key generation.
context := context.Background()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
curveName := "secp256k1"
partialSignResult, err := client.ECDSA().Sign(context, sessionConfig, keyID, nil, msgHash[:])
Enforcing a Signing Policy
As with key generation, the MPC session only takes place if all MPC nodes request this to happen. This allows you, in your application, to implement various signing policies. For example, the SDK controlling MPC node 1 may enforce a policy of transferring at most 10 BTC each day, while MPC Node 0 will require a user to press a button after inspecting the transaction details. These policies can be enforced simply by making sure that the SDKs of the MPC nodes only request the MPC signing session by calling Sign() on their SDK, once they have applied the policy.
If the signature generation MPC session is successful, each SDK obtains a partial signature. Your application must collect the partial signatures, and once collected, they can be combined into the final signature:
signature, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)
If msgHash
is provided to ECDSAFinalizeSignature()
the final signature is also validated. This lets you detect an invalid signature in the case where one of the MPC nodes was malicious and returned a bad partial signature.
Code Examples
You can find running example code for key generation and signing using Builder Vault in our demo repository here: Go, Java, node.js, C, wasm.
These examples assume that a Builder Vault TSM instance is already running locally on your machine and that the MPC nodes are reachable at the addresses http://localhost:8500
, http://localhost:8501
, http://localhost:8502
. You can follow the tutorial here to set up a TSM locally like this, or you can do this by following the instructions here. Alternatively, or you can use the demo TSM hosted by Blockdaemon, as described here.
Note that in these examples, the SDKs are invoked concurrently to start the key generation and signing MPC sessions in the example. This is because each call to the SDK blocks until all SDKs in the MPC session are called. In many real-world cases, the SDKs will run in different environments, e.g., on different servers or mobile devices.
Updated 11 days ago