RSA
In addition to threshold ECDSA and Schnorr signing, Builder Vault also supports RSA encryption and signing.
Threshold RSA Key Generation
While generating an RSA key using MPC is possible, the current state-of-the-art protocols for doing this are not very efficient. This is the reason why Builder Vault does not currently provide methods for RSA key generation in MPC. Instead, you will have to import an RSA key generated outside the Builder Vault, for example in a hardware security module (HSM), and then migrate the RSA key to Builder Vault using the import method described below.
Key Import
To import an RSA key into Builder Vault you must first secret share the key. If for example you want the key to be shared among the MPC nodes with player IDs 2, 3 and 5, and with a security threshold of one, you can do this as follows:
players := []int{2, 3, 5}
threshold := 1
sharing, err := tsmutils.RSASecretShare(threshold, players, privateKey)
Each key share must then be wrapped under the corresponding MPC nodes' public wrapping key. The wrapping key must be provided as an ASN.1 DER encoding of a SubjectPublicKeyInfo (see RFC 5280, Section 4.1), for example:
wrappingKey, err := client.WrappingKey().WrappingKey(ctx)
publicKey, err := x509.ParsePKIXPublicKey(wrappingKey)
publicRSAKey, ok := publicKey.(*rsa.PublicKey)
wrappedKeyShare, err = tsmutils.EnvelopeWrap(publicRSAKey, sharing[playerID])
Finally, the key can be imported by running a Builder Vault MPC session, where each wrapped share is input by the corresponding SDK.
keyID, err := client.RSA().ImportKey(ctx, sessionConfig, wrappedKeyShare, desiredKeyID)
If all MPC nodes in the session make this call to their SDK, providing the same desired key ID, an MPC session will start, that imports the RSA key.
The desiredKeyID
let's you suggest a key ID for the imported key. This can be useful, for example if you want to migrate a key sharing from one Builder Vault instance to another, without changing the key ID. If you provide an empty string as the desired key ID, the MPC nodes will choose a random key ID.
When to Use the Key
If an honest MPC node succeeds with the key import MPC session, the protocol ensures that the MPC node will hold a correct key share. But the protocol does not guarantee that all honest players will succeed, and some may have aborted and do not hold a key share. This may unintentionally reduce the availability of the key. In the worst case, so few players will end up holding a key share, that no operation such as decryption can be performed with the key. To avoid this, you should make sure that all MPC nodes successfully completed the key import MPC session before using the imported key.
Once the key is imported, you can obtain the public key as follows:
publicKey, err := client.RSA().PublicKey(ctx, keyID)
This will return a byte array with an ASN.1 DER encoding of SubjectPublicKeyInfo.
Trusting the Public Key
If you trust a specific MPC node, you can use the public key returned from that node. But as an external user of the Builder Vault you generally don't know which of the MPC nodes may or may not be corrupted. In that case you should request the public key from at least threshold + 1 MPC nodes and make sure that all the returned public keys are equal, before you trust the public key. Obtaining the public key from at least threshold + 1 MPC nodes ensures that you obtain the public key from at least one honest MPC node.
A complete example, showing how to import an RSA key and use it for encryption and signing, is provided at the bottom if this page.
Key Export
Exporting an RSA key is done by running an MPC session. The export MPC session only runs if all involved SDKs call the method. This gives each SDK user the opportunity to enforce whatever policy it finds necessary before allowing the export. As an additional level of security, all MPC nodes involved in the MPC session must have RSA key export enabled in their configuration.
The MPC session computes a new random secret sharing of the RSA key and one share of the new sharing is output to each SDK. Each share is wrapped before being output to the SDK, by a wrapping key provided by the SDK. Only wrapping keys that are whitelisted in the MPC node configuration is accepted.
If you want to migrate the RSA key from one Builder Vault instance to another, you can obtain the wrapping key from the target node as follows:
wrappingKey, err := targetClient.WrappingKey().WrappingKey(ctx)
publicKey, err := x509.ParsePKIXPublicKey(wrappingKey)
publicWrappingKey, ok := publicKey.(*rsa.PublicKey)
Alternatively, you can generate a wrapping key like this:
privateWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096)
publicWrappingKey, err := x509.MarshalPKIXPublicKey(&privateWrappingKey.PublicKey)
In any case, you must ensure that the public wrapping key is whitelisted in the source MPC node configuration.
res, err := client.RSA().ExportKeyShares(ctx, exportSessionConfig, keyID, publicWrappingKey)
The returned result can now be used in an import session at another Builder Vault instance. Alternatively, if you have access to threshold+1 or more key shares and their private unwrapping keys, you can recover the RSA key as follows:
// If you do this for all key shares
keyShare, err := tsmutils.EnvelopeUnwrap(privateWrappingKey, res.WrappedKeyShare)
// Then you can recover the RSA key as follows
exportedPrivateRSAKey, err := tsmutils.RSARecombine([][]byte{ keyShare1, keyShare2, keyShare3})
A complete example, showing how to export an RSA key, is given at the end of this page.
Migrating Keys without Single Point of Trust
A major goal of the Builder Vault is to eliminate any single point of trust. If someone exports and unwraps all key shares and recovers the key as shown above, that party will essentially be a single point of trust. To avoid this, it is recommended to only unwrap and recover the exported RSA key by importing the wrapped key shares into another Builder Vault instance, or in some other high-security environment, such as an HSM.
Signing
The Builder Vault supports both RSASSA-PSS signatures and RSASSA-PKCS1v15 signatures. Once an RSA key is imported into Builder Vault, you can generate an RSASSA-PSS signature of a message as follows:
message := []byte("some message to be signed")
sha256Hashed := sha256.Sum256(message)
hashedMessage := sha256Hashed[:]
hashFunction := tsm.HashFunctionSHA256
partialSignature, err := client.RSA().SignPSS(ctx, signSessionConfig, keyID, hashFunction, hashedMessage)
When the SignPSS()
method is called on the SDKs belonging to all MPC nodes in the session, a signature is generated, and a partial signature is returned to each SDK. The partial signatures must be collected and combined into the final signature as follows:
signature, err := tsm.RSAFinalizeSignaturePSS(hashFunction, hashedMessage, partialSignatures)
The hashedMessage
is optional. If provided, the final signature will be validated as part of the finalization. The salt size used in PSS is the same length as the output length of the hash function.
You can generate signatures according to RSASSA-PKCS1-V1_5-SIGN from RSA PKCS #1 v1.5 in much the same way:
// Call this concurrently on each SDK
partialSignature, err := client.RSA().SignPKCS1v15(ctx, keyID, hashFunction, hashedMessage)
// Then call this somewhere, after collecting all partial signatures returned from the SDKs
signature, err := tsm.RSAFinalizeSignaturePKCS1v15(hashFunction, hashedMessage, partialSignatures)
The supported hash functions are tsm.HashFuctionNone
, tsm.HashFunctionSHA1
, and tsm.HashFunctionSHA256
. Note that the hashedMessage
must be the result of hashing the input message using the given hash function. If hash is tsm.HashFunctionNone
the hashedMessage
is signed directly. This isn't advisable except for interoperability.
A complete example, showing how to import an RSA key and use it for encryption and signing, is provided at the bottom if this page.
Decrypting
The Builder Vault supports both OAEP, PKCS1v15 and raw decryption. Given an RSA ciphertext generated using the public RSA key, you can decrypt by having each SDK call this method concurrently:
partialDecryption, err := client.RSA().Decrypt(ctx, keyID, ciphertext)
The partial decryption results can then be collected and turned into the final plaintext using one of the following calls:
plaintext, err := tsm.RSAFinalizeDecryptionRaw(partialDecryptions)
plaintext, err := tsm.RSAFinalizeDecryptionPKCS1v15(partialDecryptions)
plaintext, err := tsm.RSAFinalizeDecryptionOAEP(hashFunction, label, partialDecryptions)
Which one to use depends on whether you produced the ciphertext using raw, PKCS1v1.5 or OAEP encryption.
A Complete Example
You can find a complete example in our demo repository (Go). The example imports an RSA key, uses it to sign and decrypt, and shows how to exports the key.
Updated 15 days ago