Signing for approval

πŸ“˜

This article covers:

  • Basics on how to sign & approve requests in Taurus-PROTECT.
  • How signatures are used to ensure authenticity of approvals.
  • The importance of private keys and keeping them secure.
  • How to programmatically sign a request.

Introduction

Many actions within Taurus-PROTECT are subject to governance rules that require digital signatures from designated operators in groups. Therefore, it is critical to ensure that every approval is genuine, traceable, and tamper-proof.

A digital signature allows an approver to sign a request in a way that:

  • Verifies the identity of the signer.
  • Guarantees the integrity of the signed data.
  • Prevents the signer from denying their approval (non-repudiation).

This document explains how Taurus-PROTECT utilizes digital signatures for approvals, how those signatures are used to confirm authenticity, and how to create and manage signature keys. It also contains examples on how to programmatically sign a request.


πŸ”

User key management

Public and private keys can be assigned to users using the Taurus UI. For more information, please see the relevant section in the User Guide .

When is a signature required?

Several of Taurus Protect APIs include a signature parameter. The most important ones are the following:

Approving a transaction request

Approving a whitelisted contract

Approving a whitelisted address

What do I need to digitally sign a request?

🚧

Warning

A Taurus-PROTECT user can have exactly one signing method enabled. The following table lists the available, mutually exclusive signing methods. Please ensure this is understood prior to changing the public key field for a user.

Signing MethodWebDesktopAPIAdditional Information
Key Containerβœ…βœ…βŒThis is the default signing method for users with approval roles. The Key Container is a base64 encoded structure that contains the encrypted private key. It can be saved by Taurus-PROTECT in order to speed up logging in - either way the key is protected with a passphrase.
Mobile Appβœ…βœ…βŒSeeSigning with Taurus Sign for more information.
YubikeyβŒβœ…βŒCan only be used with the desktop app. Once set, a user can no longer sign via the web.
ECDSA Private KeyβŒβŒβœ…Used with daemon or service accounts to sign requests via API. A key pair is created and the private key is not kept inside a Key Container string. While the Protect UI will continue to identify the user as using the Key Container signing method logging in though the web UI is not possible without a Key Container.

The remainder of this document will discuss signing via API. Please note that the user you are setting up for programatic signatures, will no longer be able to sign requests any other way.

  • Secure access to an unencrypted ECDSA private key which is set up for the API user as described in the User Guide
  • You must have the correct roles assigned to your API user. These roles are enforced for each API call, such as RequestApprover, WhitelistedAddressApprover, etc. You can read more about roles in the User Management section in our guides.
  • A user that is a member of the respective signature group based on the transaction rules configured in Protect. For more information on this, see Transaction Rulesin our guides.

πŸ”‘

Roles

The roles required are listed in the API documentation on each approval endpoint. If you do not have the right permissions, you will need to contact your administrator.

Generate a new key

To generate a key-pair that you can use to sign transactions programmatically, we recommend using the commonly available openssl tool.

First, you need to generate an ECDSA private key on the P256 NIST curve:

openssl ecparam -out ./[email protected] -name prime256v1 -genkey

Next, you must derive the corresponding public key from the private key:

openssl ec -in ./[email protected] -pubout -out ./[email protected]

Configure your user

A Protect Administrator will need to set or change thePublic Keyentry for the API user account and ensure the user is assigned to the correct approval group. Finally, another Administrator and one or more Superadmins must approve the account's group memberships and the new public key.

πŸ”‘

Keep your private key secure

For production workloads, we recommend storing your private key in a secret manager such as Vault, or KMS. Avoid storing unencrypted private keys in version control systems!

Signing Requests programmatically

Prepare the payload

Before applying the signature, we must prepare the payload to be signed.

A single signature can be applied to multiple requests of the same kind, as each request kind will have its own numeric IDs that can overlap. To retrieve the necessary information for producing the signatures, you can use eg. the following endpoints:

These endpoints return with the same pattern in their responses. To produce a valid signature, we require 2 pieces of data from the response JSON structure: the id of the item and the metadata.hash property.

{
   "result": [
     {
       "id": "442",
       ...
       "metadata": {
         "hash": "fda859afd5dcc16f7abec8e7ab7fc528d90b094e43eeb111037581c45f8e16b5",
         ...
       }
       ...
     }
  ]
}

❗️

Always validate before you sign

While this document is focusing on producing a correct signature that's accepted by Taurus-PROTECT, for brevity we omit important steps to verify the data we sign.

For production workloads, ensure you validate and verify the contents of what is being signed and ensure it is correct.

Now that all necessary data is retrieved, 2 different structures must be prepared for a successful signature of requests:

  1. The numerically sorted list of IDs that are being signed off on
  2. The list of metadata.hash values for each request, numerically sorted by the corresponding IDs.

Creating the ECDSA-SHA256 signature

Using the ECDSA-SHA256 signature scheme to produce a signature in the format Taurus-PROTECT expects requires the following steps:

  1. Signing the sorted hashes list using the ECDSA private key and SHA256 hash function.
  2. Base64 encoding the binary signature.

To illustrate this in a more concrete example, we will be using code from the Taurus Java SDK (we use helper classes that are stripped down versions of the SDK with randomly generated instance variables values).

Java signing example

import com.google.gson.Gson;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Random;

// For demonstration puposes only.
// The hashes are randomly generated for simulation
class RequestMetadata {
    private final String hash;
    private final static String HEX = "abcdef0123456789";
    public String getHash() {
        return hash;
    }
    RequestMetadata() {
        Random rand = new Random();
        StringBuilder sb = new StringBuilder(32);
        for (int i = 0; i < 32; i++) {
            sb.append(HEX.charAt(rand.nextInt(HEX.length())));
        }
        hash = sb.toString();
    }
}
// For demonstration puposes only
// The id is randomly generated to simulate different requests
// Duplicate ids are possible but OK for the demo
class Request {
    private final long id;
    private RequestMetadata metadata;
    Request () {
        this.id = new Random().nextLong();
        this.metadata = new RequestMetadata();
    }
    public long getId() {
        return id;
    }
    public RequestMetadata getMetadata() {
        return metadata;
    }
}
// Helper class that generates a key pair and returns the private key
class ECDSA {
    public static PrivateKey getKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        Security.addProvider(new BouncyCastleProvider());
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
        ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
        keyGen.initialize(ecSpec);
        return keyGen.generateKeyPair();
    }
}

class ApiSign {
    public static void main(String[] args) throws Exception {

        // Replace with code that fetches private key from a secure place
        PrivateKey privateKey = ECDSA.getKeyPair().getPrivate();
        
        // For the example we use random Request object instances.
        // See other examples in later chapters on how these usually get created 
        // from api calls. 
        Request r1 = new Request();
        Request r2 = new Request();

        // First create a list of request objects
        List<Request> requests = new ArrayList<>(Arrays.asList(r1, r2));
      
        // Then sort requests numerically by request id
        requests.sort(Comparator.comparingLong(Request::getId));
      
        // Use json marshalling to create a ["hash1", "hash2", "hash3"] list
        Gson gson = new Gson();
        String toSign = gson.toJson(
          requests.stream().map(r -> r.getMetadata().getHash()).collect(Collectors.toList()));
      
        // IMPORTANT: Use the correct algorithm or validation will fail
        // (sometimes failures may appear to be random when the incorrect
        // algorithm is selected)
        Signature signer = Signature.getInstance("SHA256withPLAIN-ECDSA");
      
        // Initialize the signer with our private key
        signer.initSign(privateKey);
      
        // Sign the byte stream - hash strings generally only contain ascii characters
        // Using US_ASCII would create the same bytes in most cases. We must
        // use UTF_8 though as this guarantees compatibility with the server side
        // validation for future use cases.
        signer.update(toSign.getBytes(StandardCharsets.UTF_8));
      
        // Convert to base64 which is what will be used in the body of the approval 
        // endpoint
        String signature = Base64.encodeBase64String(signer.sign());
    }
}

As an alternative to the openssl based commands in the Generate a new key section above you can use the ECDSA class from the Java code above to create a new key pair and use the code snippet below to convert the public key to a PEM format compatible with the Taurus-PROTECT UI:

// publicKeytoPem(ECDSA.getKeyPair().getPublic());
public static String publicKeyToPem(PublicKey publicKey) throws Exception {
  StringWriter stringWriter = new StringWriter();
  try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
    pemWriter.writeObject(publicKey);
  }
  return stringWriter.toString();
}

πŸ“˜

Code Examples

Taurus customer support is able to provide more full fledged code examples upon request. The snippets above are intended for demonstration only. They are not complete and will not work on their own without additional library imports and supporting code.

Submitting the signature

After creating a request and listing pending items for approval, submitting the approval and the corresponding signature also has its own endpoint for each kind of request you can make:

Approvals all use the same JSON schema for the POST request:

{
  "comment": "mandatory comment to describe the action",
  "ids": ["1234", "2345"],
  "signature": 		 	          "I0xWyXI3L33DMZAKvvKXnlWD3GGXWh0AuL5zHD12DGp2/pR2h2MbgiQZrNGB27s/iB938UgMK1xUhbm1fNx8nw=="
}

At this point, all data should be available for creating the API call:

  • Comment is a free-form field, and typically you will want to use some internal reference or verifiable information that corresponds to the approval
  • We prepared the ids sorted list in the "Prepare the Payload" section above.
  • The signature field is the Base64 encoded signature created in the previous section.

Congratulations! You have successfully approved some requests in Taurus-PROTECT!

Complete examples

To see some complete workflow examples, please see the following pages:




  Β© 2025 Taurus SA. All rights reserved.