Skip to main content

Duo Server

Silent Shard Duo Server is the backend service that handles the cryptographic operations and provides necessary APIs. It acts as the second party in the MPC protocol along with the mobile SDK.

The company deploying the SDK should host the server on their own infrastructure. We provide a Docker image for the server that can be deployed on any cloud provider.

Deploying the server

Our server image is available in a private Docker registry: ghcr.io/silence-laboratories/duo-server. It is using file system as storage for the keyshares.

Getting access to Docker Registry

Generate GitHub Personal Access Token

Get a Personal Access Token from GitHub if you don't have one.

Guide to generate GitHub Personal Access Token
  1. Go to GitHub Personal Access Tokens
  2. Click on "Generate new token", we are using the classic token for this guide.
  3. Enter a name for the token
  4. Select the "read::packages Download packages from GitHub Package Registry" scope.
  5. Generate the token.

Login to Docker Registry

Login to docker using Personal Access Token and your GitHub username.

echo <PAT> | docker login ghcr.io -u <username> --password-stdin

Save docker compose file to launch:

Save it as docker-compose.yaml
services:
duo-server:
image: ghcr.io/silence-laboratories/dkls23-rs/duo-server:${IMG_TAG}
command: /usr/local/bin/sigpair-node
environment:
RUST_LOG: info
LISTEN: 0.0.0.0:8080
DEV_MASTER_SIGN_KEY: /run/secrets/duo-signing-master-key
FILE_STORAGE_URL: file:///data/
ports:
- 8080:8080
volumes:
- duo-server-data:/data
secrets:
- duo-signing-master-key

volumes:
duo-server-data:

secrets:
duo-signing-master-key:
file: ./testdata/party_0_sk

Before launch, prepare environment variables by calling following script:

Save it as setup-env.sh
#!/bin/bash

ENV_FILE=env-file

mkdir -p ./testdata
rm -f ${ENV_FILE}

echo IMG_TAG=v5 > ${ENV_FILE}

# Generate keys used by parties to sign their MPC messages
# generate 32 random bytes and store them under ./testdata/party_0_sk
openssl rand 32 > ./testdata/party_0_sk
# transform those random bytes to DER format
{
printf "\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20"
cat ./testdata/party_0_sk
} > private_key_0.der
# get public key in DER format, extract from that hex representation of public key
PUBLIC_KEY_HEX=$(openssl pkey -in private_key_0.der -pubout -outform DER | xxd -p -s 12 -c 64)
echo SL_FIRST_PARTY_PUBLIC_KEY=${PUBLIC_KEY_HEX} >> ${ENV_FILE}

# delete the private key in DER format
rm private_key_0.der


# Generate secret key for user
openssl rand 32 > ./testdata/party_2_sk

echo "Environment variables stored in $ENV_FILE"

Run the script with:

shell
chmod +x setup-env.sh
./setup-env.sh

The script will fill up env-file with the environment variables for the server.

Now let's start the server by running

shell
docker compose --env-file env-file up

If you see these logs, you have set up the server successfully.

Attaching to duo-server-1
duo-server-1 | WARN: The dotenv file is not set
duo-server-1 | 2025-09-16T09:59:09.200234Z INFO sigpair_node: Creating SimpleStorage
duo-server-1 | 2025-09-16T09:59:09.201975Z INFO sigpair_node: Party VK <YOUR_CLOUD_VERIFYING_KEY_HEX_STRING>
duo-server-1 | 2025-09-16T09:59:09.202413Z INFO sigpair_node: listening on 0.0.0.0:8080

Great! The server is now up and running to perform MPC actions with our mobile.

Connect to the server using a CLI client

As a quick test, use following script which acts as the second party to test all the exposed APIs of your running Duo Server.

Save it in run-test.sh
#!/bin/bash
set -e

echo "
██████╗ ██╗ ██╗ ██████╗
██╔══██╗██║ ██║██╔═══██╗
██║ ██║██║ ██║██║ ██║
██║ ██║██║ ██║██║ ██║
██████╔╝╚██████╔╝╚██████╔╝
╚═════╝ ╚═════╝ ╚═════╝
"

# Load server party public keys
source ./env-file

STORE="./testdata/fs/sigpair/client"
mkdir -p "${STORE}"

export RUST_LOG=info
cmd="docker run --rm --name duo-client --network=host \
-v ./testdata:/app/testdata \
--user $(id -u):$(id -g) \
ghcr.io/silence-laboratories/dkls23-rs/duo-server:${IMG_TAG} /usr/local/bin/sigpair-embedded"

SERVER="localhost:8080"
SERVER_ARGS=""
DEVICE_KEY_ARGS=""


# ECDSA keygen (2-of-2)
$cmd key-gen \
--share "${STORE}/duo.client.share" \
--sign-algo ecdsa \
--user-sk "./testdata/party_2_sk" \
--server-vk "${SL_FIRST_PARTY_PUBLIC_KEY}" \
$SERVER_ARGS \
$DEVICE_KEY_ARGS
echo "keygen ok"

# ECDSA sign
$cmd sign-gen \
--share "${STORE}/duo.client.share" \
--sign-algo ecdsa \
--message-hash "${SL_FIRST_PARTY_PUBLIC_KEY}" \
--chain-path "m" \
--user-sk "./testdata/party_2_sk" \
--server-vk "${SL_FIRST_PARTY_PUBLIC_KEY}" \
$SERVER_ARGS \
$DEVICE_KEY_ARGS
echo "sign ok"

# run key generation 2x2
$cmd key-gen \
--old-share ${STORE}/duo.client.share \
--share ${STORE}/duo.client-1.share \
--sign-algo ecdsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
$SERVER_ARGS \
$DEVICE_KEY_ARGS


echo "key refresh ok"


# run signature generation
$cmd sign-gen \
--share ${STORE}/duo.client-1.share \
--sign-algo ecdsa \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
$SERVER_ARGS \
$DEVICE_KEY_ARGS


echo "sign after refresh ok"

# run signature generation
$cmd pre-sign \
--share ${STORE}/duo.client-1.share \
--pre-sign ${STORE}/duo.pre-sign.1 \
--sign-algo ecdsa \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "pre-sign ok"

$cmd finish \
--pre-sign ${STORE}/duo.pre-sign.1 \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}


echo "finish ok"

# run key generation 2x2
$cmd key-gen \
--share ${STORE}/duo.client-http.share \
--sign-algo ecdsa \
--no-ws true \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}

echo "http key-gen ok"


# run signature generation
$cmd sign-gen \
--share ${STORE}/duo.client-http.share \
--no-ws true \
--sign-algo ecdsa \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}


echo "sign using http key-share ok"

# run signature generation
$cmd pre-sign \
--share ${STORE}/duo.client-http.share \
--no-ws true \
--pre-sign ${STORE}/duo.pre-sign-http.1 \
--sign-algo ecdsa \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}

echo "pre-sign http ok"

# Export ECDSA private key
private_key_ecdsa=$(RUST_LOG=info $cmd key-export \
--share ${STORE}/duo.client-1.share \
--sign-algo ecdsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER})

echo "ecdsa key export ok"


# Run key import using the exported ECDSA key
$cmd key-import \
--private-key "${private_key_ecdsa}" \
--out-share ${STORE}/duo.client.ecdsa-imported \
--sign-algo ecdsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "ecdsa key import ok"

# Test signing with the imported ECDSA key
$cmd sign-gen \
--share ${STORE}/duo.client.ecdsa-imported \
--sign-algo ecdsa \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "ecdsa sign with imported key ok"

# Test standalone reconcile with ECDSA (happy path)
$cmd reconcile \
--share ${STORE}/duo.client-1.share \
--sign-algo ecdsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "ecdsa reconcile ok"

# Create a new RSA Key pair using openssl
# Generate RSA public key for backup
openssl genpkey -algorithm RSA -out ${STORE}/private_key.pem -pkeyopt rsa_keygen_bits:2048 2>/dev/null
recv_enc_pubkey=$(openssl rsa -in ${STORE}/private_key.pem -RSAPublicKey_out 2>/dev/null)

# Test ECDSA backup functionality
$cmd backup \
--share ${STORE}/duo.client-1.share \
--sign-algo ecdsa \
--rsa-pubkey "${recv_enc_pubkey}" \
--label "ecdsa-backup-test" \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}

echo "ecdsa backup ok"

# run key generation 2x2
$cmd key-gen \
--share ${STORE}/duo.client.ed-share \
--sign-algo eddsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa keygen ok"

# run signature generation
$cmd sign-gen \
--share ${STORE}/duo.client.ed-share \
--sign-algo eddsa \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa sign ok"

# run key refresh 2x2
$cmd key-gen \
--old-share ${STORE}/duo.client.ed-share \
--share ${STORE}/duo.client.ed-share-refresh \
--sign-algo eddsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa key refresh ok"

# Test EdDSA backup functionality
$cmd backup \
--share ${STORE}/duo.client.ed-share-refresh \
--sign-algo eddsa \
--rsa-pubkey "${recv_enc_pubkey}" \
--label "eddsa-backup-test" \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER}

echo "eddsa backup ok"

private_key=$(RUST_LOG=info $cmd key-export \
--share ${STORE}/duo.client.ed-share-refresh \
--sign-algo eddsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server http://${SERVER})

echo "eddsa key export ok"


# Run key import using the exported key
$cmd key-import \
--private-key "${private_key}" \
--out-share ${STORE}/duo.client.ed-share-imported \
--sign-algo eddsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa key import ok"

# Test signing with the imported key
$cmd sign-gen \
--share ${STORE}/duo.client.ed-share-imported \
--sign-algo eddsa \
--message-hash cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4 \
--chain-path "m" \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa sign with imported key ok"

# Test standalone reconcile with EdDSA (happy path)
$cmd reconcile \
--share ${STORE}/duo.client.ed-share-refresh \
--sign-algo eddsa \
--user-sk ./testdata/party_2_sk \
--server-vk ${SL_FIRST_PARTY_PUBLIC_KEY} \
--server ws://${SERVER}

echo "eddsa reconcile ok"

Run the script with:

shell
chmod +x run-test.sh
./run-test.sh

If you see these logs, you have tested the MPC operations successfully.

...
keygen ok
...
sign ok
...
key refresh ok

Environment variables for Duo Server


Please note the environment variables in docker-compose file:

  • FILE_STORAGE_URL: The URI of the file storage that the server will use to store the keyshare.
  • DEV_MASTER_SIGN_KEY: The path to the file that contains the server's master sign key.
  • LISTEN: Server address to listen on.

Private Endpoints

Sensitive operations for backup and export are not publicly available to the SDK. These endpoint are meant to be used by the company deploying the SDK to expose these endpoints to the user in a secure way with extra authentication logic.

tip

Example case:

  • When the user requests a backup/export, the backend can request additional authentication from the user (e.g., 2FA) before responding.
  • Once the user is authenticated, the backend can get the verifiable backup/export from the cloud node and pass it to the user.
  • /v3/[ecdsa|eddsa]/backup: Generate a verifiable backup of the server's keyshare according to the specified algorithm.
  • /v3/[ecdsa|eddsa]/export: Export the full private key of the MPC wallet according to the specified algorithm.

Verifiable Backup

  • Description: Generate a verifiable backup of the server's keyshare.
  • HTTP Method: POST
  • URL: http://{SERVER_URL}/v3/[ecdsa|eddsa]/backup
  • Request Body:
    • key_id: The key ID of the keyshare.
    • rsa_pubkey_pem: The RSA public key in PEM format.
    • label: The label used as associated data while performing RSA encryption of the server's keyshare. The label is required while decrypting/verifying the backup.
{
"key_id": "KEY_ID",
"rsa_pubkey_pem": "RSA_PUBLIC_KEY_PEM",
"label": "BACKUP_LABEL"
}
  • Response Body:
    • key_id: The key ID of the keyshare.
    • algo: The signing algorithm of the Keyshare (ecdsa or schnorr).
    • verifiable_backup: The verifiable encrypted backup of the server's keyshare. Base64 encoded string.
Example Response Body
{
"key_id": "D4K6eBI9SIbem4mxRONtBuox+5Y0JuhyosGcbjcAi5E=",
"algo": "ecdsa",
"verifiable_backup": "HJ4SSOxGJ/TZwLyKKWAS+OO1yTF14M8JWFiw9/ncS/7qt+oLn5xR+IFBu2Qka9FyP1K5PIoUl72UWOZbJ4m/BIL4fjs04Ru+UeqQkICYl2B6qf5YySFSHtZbLAjR3pk81q+eD/PH5eVA5jwf0ntNrxg9Fy3zbaUpv7MitAaTzEQ="
}

Export Key

  • Description: Export the full private key of the MPC wallet.
  • HTTP Method: POST
  • URL: http://{SERVER_URL}/v3/[ecdsa|eddsa]/export
  • Request Body:
    • key_id: The key ID of the keyshare.
    • client_enc_key: The encryption public key of the client. 32 byte Base64 encoded string.
Request Body
{
"key_id": "KEY_ID",
"client_enc_key": "CLIENT_ENC_KEY"
}
  • Response Body:
    • key_id: The key ID of the keyshare.
    • server_public_key: The server's encryption public key in bytes. Required for decrypting the backup.
    • enc_server_share: The encrypted server share as a Base64 encoded string.
Example Response Body
{
"key_id": "D4K6eBI9SIbem4mxRONtBuox+5Y0JuhyosGcbjcAi5E=",
"server_public_key": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31
],
"enc_server_share": "HJ4SSOxGJ/TZwLyKKWAS+OO1yTF14M8JWFiw9/ncS/7qt+oLn5xR+IFBu2Qka9FyP1K5PIoUl72UWOZbJ4m/BIL4fjs04Ru+UeqQkICYl2B6qf5YySFSHtZbLAjR3pk81q+eD/PH5eVA5jwf0ntNrxg9Fy3zbaUpv7MitAaTzEQ="
}