GO SDK

How to obtain the Go SDK

You first need to set up HTTPS authentication to our GitLab repository. For this you need to obtain a username and password from Blockdaemon. Then you can do as follows:

export GOPRIVATE=gitlab.com/sepior
echo "machine gitlab.com login [YOUR_USERNAME] password [YOUR_PASSWORD]" >> $HOME/.netrc

You can then fetch the Go SDK like this:

go mod init example.com
go get gitlab.com/sepior/go-tsm-sdk

If you need a specific version of the SDK, e.g. version 52.1.1, you can do

go get gitlab.com/sepior/[email protected]

You can see a list of versions here.

Usage

The Go SDK can be used directly in Go applications by source or compiled into a shared library for most platforms. In particular, it can be compiled into a WebAssembly library.

Below we illustrate how the SDK can be used in an application.

Authentication

To start using the SDK you must first provide credentials for authenticating to the TSM.
You must authenticate to each TSM node:

package main

import (
    "net/url"
    "gitlab.com/sepior/go-tsm-sdk/sdk/tsm"
)

func main() {

    url1, _ := url.Parse("https://tsm-node1.example.com")
    url2, _ := url.Parse("https://tsm-node2.example.com")
    url3, _ := url.Parse("https://tsm-node3.example.com")

    userID := "myUser"

    authenticator1 := tsm.PasswordAuthenticator{Username: userID, Password: "password1"}
    authenticator2 := tsm.PasswordAuthenticator{Username: userID, Password: "password2"}
    authenticator3 := tsm.PasswordAuthenticator{Username: userID, Password: "password3"}

    nodes := []tsm.Node{
            tsm.NewURLNode(*url1, &authenticator1),
            tsm.NewURLNode(*url2, &authenticator2),
            tsm.NewURLNode(*url3, &authenticator3),
        }

    client := tsm.NewClient(3,1,nodes)
    _ = client

}

πŸ“˜

Note:

Please don't forget to update the https://tsm-node.example.com, password, and myUser with the URLs, passwords, and userID that you have in your credentials JSON file.

From the client variable (of type tsm.Client) you can derive clients for specific algorithms e.g. ECDSA, ED25519, AES, HMAC, RSA, etc.

ECDSA Key Generation

Generating a key is quite straightforward:

ecdsaClient := tsm.NewECDSAClient(client)

keyID, err := ecdsaClient.Keygen("secp256k1")

ECDSA Signing

Once a ECDSA key has been generated it can be used for signing. Let us say that the key generated above has keyID "ifyCwxrzzEaM3ePD40j6LtzQ7xo":

keyID := "ifyCwxrzzEaM3ePD40j6LtzQ7xo"
chainPath := []uint32{4, 1, 1, 1, 2}
message := []byte("Hello World")
hash := sha256.Sum256(message)

signature, recoveryID, err := ecdsaClient.Sign(keyID, chainPath, hash[:])

ECDSA Key Import

In some cases it might be desirable to import an existing ECDSA private key key. The first step in importing a private key is to split it into as many shares as there are TSM nodes. This can be done with the following method from the SDK:

ecdsaPrivateKeyShares, err := tsm.ECDSASecretShare(nodeCount, threshold, "secp256k1", ecdsaPrivateKey)

Each TSM node has a wrapping key. Before importing the shares of the private key they must be encrypted with the wrapping keys:

ecdsaClient := tsm.NewECDSAClient(client)
wrappingKeys, wrappingKeyFingerprints, err := ecdsaClient.WrappingKeys()
...

wrappedPrivateKeyShares := make([][]byte, nodeCount)
for i := 0; i < nodeCount; i++ {
	wrappingKey, err := x509.ParsePKIXPublicKey(wrappingKeys[i])
  ...
  if rsaWrappingKey, isRSAKey := wrappingKey.(*rsa.PublicKey); isRSAKey {
  	wrappedPrivateKeyShares[i], err = rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaWrappingKey, ecdsaPrivateKeyShares[i], nil)
    ...
  }
}

Once the key shares are wrapped they can be imported. In addition to the wrapped key shares, key import takes the name of the elliptic curve, an array of wrapped chain codes, a DER encoding of the ECDSA public key and an optional keyID.

// Wrap the chain code in the same way as the key shares (RSA-OAEP). Leave as nil if no chain code is needed.
var wrappedChainCodes [][]byte

// If you want a specific value for the keyID of the imported key set it here. Otherwise a new keyID is automatically generated.
var keyID string

keyID, err = ecdsaClient.ImportWrappedKeyShares("secp256k1", wrappedPrivateKeyShares, wrappedChainCodes, derEncodedPublicKey, keyID)

ECDSA Partial Recovery Info

Once an ECDSA key has been generated it is possible to export it encrypted under a public key of an emergency recovery system. The ersPublicKey must be an PKIX encoded RSA public key. The label can be any data, but it must be known by the ERS when recovering the secret key.

ecdsaClient := tsm.NewECDSAClient(client)

sessionID := tsm.GenerateSessionID()
recoveryInfos := ecdsaClient.PartialRecoveryInfo(sessionID, keyID, ersPublicKey, label)

The returned recoveryInfos will be an array with one entry for each node the SDK is configured with. If the SDK is not configured with all nodes, then you first need to generate the sessionID, communicate the sessionID to each node out of band and then call PartialRecoveryInfo on each node. Finally you must take all recoveryInfos and combine them to one array with the same length as there are nodes in the TSM.

Once recoveryInfos contain recovery information from each node, you can verify them and get the final recovery data which can be used by the ERS to recover the secret key. You also need the ersPublicKey and label for this step, but no communication with the TSM nodes is needed.

recoveryData, err := tsm.RecoveryInfoCombine(recoveryInfos, ersPublicKey, label)

ECDSA Recovery Info

This is similar to the partial recovery information above, but here node 0 will get the final recovery data and the other nodes get nothing. This is normally only useful in the case where node 0 is a mobile client.

This must be called on all nodes with the same sessionID:

recoveryInfo, err := ecdsaClient.RecoveryInfo(sessionID, keyID, ersPublicKey, label)

Encryption

We support AES in different modes of operation. While useful for some purposes MPC-based AES is still quite slow compared to traditional AES encryption on a single machine.
Blockdaemon has its own patented threshold cipher that has the property that the encryption key is shared and never combined.
An encryption key is generated much as before:

sepiorPrfClient := tsm.NewPRFClient(client)

keyID, err := sepiorPrfClient.Keygen("secp256k1")

And to use the key for encryption:

keyID := "-600Jaq9ms0psummv0dlHrj0Sng"
message := []byte("Hello World")
var iv [16]byte
_, _ = rand.Read(iv[:])

keystream, err := sepiorPrfClient.Keystream(keyID, iv[:], len(message))

var encrypted []byte
for i := range message {
	encrypted = append(encrypted, message[i]^keystream[i])
}