Skip to main content

Get started with Duo Server

Prerequisites

Before you begin, ensure you have completed the following steps:

  1. Set up a Firebase Project and SSO:
  2. Get access to the Duo server:
    • To use the server, you need to have access of the package ghcr.io/silence-laboratories/dkls23-rs/duo-server:v5. Contact a Silence Laboratories team to get access.
  3. Obtain NPM Token from Silence Laboratories:
    • To install the Silent Shard SDK, you will need an NPM token provided by the Silence Laboratories team. Please contact them directly to obtain this token.

Login to Docker

  • Login to docker using Personal Access Token(PAT) and your username.
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.
info

Learn more about GitHub Personal Access Tokens here.

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

Up and running Duo Server with Auth Service

You can run the Duo server with Auth Service using Docker. Below is a sample docker-compose.yml file that sets up the necessary services.

Save it as docker-compose.yaml
# docker-compose.yml
version: "3.9"

networks:
duo-net:
driver: bridge

services:
duo-server:
image: ghcr.io/silence-laboratories/dkls23-rs/duo-server:v5
container_name: duo-server
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/"
# Auth hooks configuration
AUTH_HOOK_TYPE: "webhook"
AUTH_DKG_SETUP_VALIDATOR_URL: "http://auth-svc:9090/hook/dkg_setup_validation"
AUTH_DSG_SETUP_VALIDATOR_URL: "http://auth-svc:9090/hook/dsg_setup_validation"
AUTH_KEY_ID_NOTIFICATION_URL: "http://auth-svc:9090/hook/key_id_notification"
ports:
- "8080:8080"
volumes:
- duo-server-data:/data
secrets:
- duo-signing-master-key
networks:
- duo-net

auth-svc:
container_name: auth-svc
image: ghcr.io/silence-laboratories/two-party-boilerplate-backend:06373aaf849597efea2367e7e4f3411359ac311e
ports:
- "9090:9090"
volumes:
- ./serviceAccountKey.json:/app/serviceAccountKey.json:ro
environment:
SECRET_KEY: ${DJANGO_SECRET_KEY} # MUST be set to a secure value for prod
FIREBASE_CERT: "/app/serviceAccountKey.json"
FEATURE_MOCK_FIREBASE: "false"
FEATURE_KEYID_LOOKUP_BY_TOKEN: "true"
FEATURE_ALLOW_ED25519: "true"
depends_on:
- duo-server
networks:
- duo-net


volumes:
duo-server-data:

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

Setup environment variables

Before launch, prepare environment variables by calling following script:

Save it as setup-env.sh and run with ./setup-env.sh
#!/bin/bash
set -e

ENV_FILE=env-file

rm -f $ENV_FILE
mkdir -p ./testdata
# Generate duo server signing key
openssl rand 32 > ./testdata/party_0_sk

# Configure Django secret key, used as a seed during secrets derivation by Django
echo DJANGO_SECRET_KEY=$(openssl rand -base64 64 | tr -dc 'a-z0-9!@#$%^&*(-_=+)' | head -c 48) >> ${ENV_FILE}

echo "Environment variables stored in $ENV_FILE"

The script will fill up env-file.

Obtain serviceAccountKey.json

info

serviceAccountKey.json is the Firebase service account key. This key is crucial for the Duo Auth Service server to securely communicate with your Firebase project and verify user identities.

How to obtain serviceAccountKey.json

Step 1: Open project settings.

firebase-project-settings

Step 2: Open the Service accounts tab and click Generate new private key.

firebase-service-account

Step 3: Save the file as serviceAccountKey.json in the same directory as your docker-compose.yml file.

Start services

docker compose --env-file env-file up

If everything was setup correctly, the server should be running with these logs

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 setup to perform MPC actions with our mobile.

In logs you will notice a hex string of length 66 (33 bytes). This is the YOUR_CLOUD_VERIFYING_KEY_HEX_STRING which will be used by server to verify the requests from other party (In this case, our mobile app).

The Auth-svc server will be accessible at http://localhost:9090. Duo-server will be accessible at http://localhost:8080.

  • The YOUR_AUTH_SERVICE_ENDPOINT for iOS applications is 0.0.0.0:9090 and for Android applications is 10.0.2.2:9090.
  • The YOUR_CLOUD_NODE_ENDPOINT for iOS applications is 0.0.0.0:8080 and for Android applications is 10.0.2.2:8080.

Get started on MPC

This guide showcases the @silencelaboratories/silent-shard-sdk for key generation and signing, secured by the new hook-based authentication system.

Install the dependencies

npm install @silencelaboratories/silent-shard-sdk @silencelaboratories/dkls-sdk @silencelaboratories/schnorr-sdk @silencelaboratories/react-native-secure-key react-native-nitro-modules buffer

Configuration Values

You'll need to replace these placeholder values with actual values from your setup:

  • YOUR_CLOUD_NODE_ENDPOINT: The cloud node endpoint (e.g., localhost:8080)
  • YOUR_AUTH_SERVICE_ENDPOINT: The auth service endpoint (e.g., localhost:9090)
  • YOUR_CLOUD_NODE_VERIFYING_KEY_HEX: The public key of your cloud node, which can be obtained from your Duo Auth Proxy server configuration

Integrate with your React Native app

Create Secure Key in TEE

Secure key is used for identification of the mobile device and secure communication between mobile and cloud node. Using @silencelaboratories/react-native-secure-key library to create ECDSA keypair in the TEE.

  • Android uses Android Keystore
  • iOS uses Secure Enclave

Let's create a secure key with alias my-key. We need to access this key later to register the device to Auth Service and create a message signer for the duo session for mobile SDK.

Creating Secure Key
import * as SecureKey from "@silencelaboratories/react-native-secure-key";

const KEY_ALIAS = "my-key";

export default function App() {
React.useEffect(() => {
SecureKey.createIfNotExistSecureKey(KEY_ALIAS);
}, []);
}

Create Auth service to handle registrations

AuthService contains APIs to register user and device. Let's create a service to handle the user and device registration.

Create a file AuthService.ts in the services folder.

Show AuthService.ts code
import * as SecureKey from "@silencelaboratories/react-native-secure-key";
import { Buffer } from "buffer";
import { firebaseAuth } from "@/services/FirebaseAuthService";

const YOUR_AUTH_SERVICE_ENDPOINT = "http://192.168.1.16:9090";

export class AuthUser {
private static async createHttpClient(url: string, options?: RequestInit) {
const firebaseIdToken = await firebaseAuth.getFirebaseIdToken();
if (!firebaseIdToken) {
throw new Error("No Firebase ID token available");
}
const endpointUrl = url.replace(/^\/+/g, ""); // Remove leading slashes
return fetch(`${YOUR_AUTH_SERVICE_ENDPOINT}/${endpointUrl}`, {
...options,
headers: {
Authorization: `Bearer ${firebaseIdToken}`,
},
});
}

static async registerUser() {
const firebaseIdToken = await firebaseAuth.getFirebaseIdToken()!;
const registerUserRes = this.createHttpClient("/auth/register_user", {
method: "POST",
body: JSON.stringify({
firebaseIdToken,
}),
});

const response = await registerUserRes;
if (!response.ok) {
throw new Error("Failed to register user");
}

const data = await response.json();
if (!data.success) {
throw new Error("User registration unsuccessful");
}
return true;
}

static async registerDevice(deviceKeyAlias: string): Promise<boolean> {
const deviceVk = SecureKey.getSecureKey(deviceKeyAlias);
if (!deviceVk) {
throw new Error("Failed to get device public key");
}
const deviceVKHex = Buffer.from(deviceVk, "base64").toString("hex");

const challengeRes = await this.createHttpClient(
`/auth/challenge/${deviceVKHex}`
);
if (!challengeRes.ok) {
throw new Error("Failed to get challenge from server");
}
const challengeData = await challengeRes.json();
const signedChallenge = SecureKey.sign(
deviceKeyAlias,
Buffer.from(
JSON.stringify({ challenge: challengeData.challenge }),
"utf-8"
).toString("base64")
);
const signedChallengeHex = Buffer.from(signedChallenge, "base64").toString(
"hex"
);

const registerRes = await this.createHttpClient(`/auth/register_device`, {
method: "POST",
body: JSON.stringify({
device_id: deviceVKHex,
signature: signedChallengeHex,
}),
});
if (!registerRes.ok) {
throw new Error("Failed to register device");
}
const registerData = await registerRes.json();
if (!registerData.success) {
throw new Error("Device registration unsuccessful");
}
return true;
}
}
info

The @silencelaboratories/react-native-secure-key library return and accept Base64 encoded strings. So we need to convert it to hex string using Buffer package.

Create a session object with messageSigner which will handle MPC operations.

Mobile and Cloud node sign the MPC messages with it's own private key and verify message with public key of the other party. This ensures that the messages are not tampered in transit and are coming from a valid party.

We need to create a messageSigner using the secure key created earlier and pass it to the createEcdsaDuoSession function.

Creating a session
import * as SecureKey from "@silencelaboratories/react-native-secure-key";
import {
CloudWebSocketClient,
createEcdsaDuoSession,
} from "@silencelaboratories/silent-shard-sdk";

const KEY_ALIAS = "my-key";

const createSession = async () => {
const messageSigner = SecureKey.createMessageSigner(KEY_ALIAS);
const client = new CloudWebSocketClient("YOUR_CLOUD_NODE_ENDPOINT", false);

const session = await createEcdsaDuoSession({
client,
cloudVerifyingKey: "YOUR_CLOUD_NODE_VERIFYING_KEY_HEX",
messageSigner,
});

return session;
}

Configure the YOUR_CLOUD_NODE_ENDPOINT and YOUR_CLOUD_NODE_VERIFYING_KEY_HEX with previously obtained values from the Docker compose logs.

Test out MPC operations

With the session object is configured, you can now perform key generation and signing.

Adding MPC operations
const session = await createSession();

const keyshare = await session.keygen();

const signature = await session.sign({
keyshare,
// Keccak256 Hash("Trusted Third Parties are Security Holes")
messageHash: "53c48e76b32d4fb862249a81f0fc95da2d3b16bf53771cc03fd512ef5d4e6ed9"
});

Full example

  • Secure key creation
  • Google Sign-In and Firebase authentication
  • User and device registration to Auth Service
  • Key and signature generation using Silent Shard SDK
  • With some UI to show the status and results

Full example code for app/(tabs)/index.tsx:

index.tsx code
import { AuthUser } from "@/services/AuthSevice";
import { firebaseAuth } from "@/services/FirebaseAuthService";
import * as SecureKey from "@silencelaboratories/react-native-secure-key";
import {
CloudWebSocketClient,
createEcdsaDuoSession,
} from "@silencelaboratories/silent-shard-sdk";
import { StatusBar } from "expo-status-bar";
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

const KEY_ALIAS = "my-key";

type MpcInfo = {
isPending: boolean;
keygen?: {
time: number;
publicKeyHex: string;
};
sign?: {
time: number;
signatureHex: string;
};
};

type AuthStatus = {
loggedIn: boolean;
isPending: boolean;
};

const createSession = async () => {
const messageSigner = SecureKey.createMessageSigner(KEY_ALIAS);
const client = new CloudWebSocketClient("YOUR_CLOUD_NODE_ENDPOINT", false);

const session = await createEcdsaDuoSession({
client,
cloudVerifyingKey: "YOUR_CLOUD_NODE_VERIFYING_KEY_HEX",
messageSigner,
});

return session;
};

export default function Index() {
const [authStatus, setAuthStatus] = React.useState<AuthStatus>({
loggedIn: false,
isPending: false,
});
const [mpcInfo, setMpcInfo] = React.useState<MpcInfo>({
isPending: false,
});

React.useEffect(() => {
SecureKey.createIfNotExistSecureKey(KEY_ALIAS);
}, []);

const handleLogin = React.useCallback(async () => {
try {
setAuthStatus((p) => ({ ...p, isPending: true }));
await firebaseAuth.signInWithGoogle();
await AuthUser.registerUser();
await AuthUser.registerDevice(KEY_ALIAS);
setAuthStatus((p) => ({ ...p, loggedIn: true }));
} catch (e) {
console.error("Login failed:", e);
} finally {
setAuthStatus((p) => ({ ...p, isPending: false }));
}
}, []);

const handleMpcOperations = React.useCallback(async () => {
try {
setMpcInfo({ isPending: true });
const session = await createSession();

let startTime = Date.now();
const keyshare = await session.keygen();
const keygenTime = Date.now() - startTime;

startTime = Date.now();
const signature = await session.sign({
keyshare,
// Keccak256 Hash("Trusted Third Parties are Security Holes")
messageHash:
"53c48e76b32d4fb862249a81f0fc95da2d3b16bf53771cc03fd512ef5d4e6ed9",
});
const signTime = Date.now() - startTime;

setMpcInfo((p) => ({
...p,
keygen: { time: keygenTime, publicKeyHex: keyshare.publicKeyHex },
sign: { time: signTime, signatureHex: signature },
}))

await keyshare.free();
} catch (e) {
console.error("MPC operations failed:", e);
} finally {
setMpcInfo((p) => ({ ...p, isPending: false }));
}
}, []);

return (
<View style={styles.appContainer}>
<Text style={styles.title}>Silent Shard SDK + Auth Service </Text>

<View style={styles.container}>
{!authStatus.loggedIn && (
<Pressable
disabled={authStatus.isPending}
onPress={handleLogin}
style={({ pressed }) => [
styles.buttonContainer,
pressed && styles.buttonPressed,
]}
>
<Text style={styles.buttonText}>Sign in with Google</Text>
</Pressable>
)}

{authStatus.loggedIn && (
<>
<Pressable
disabled={mpcInfo.isPending}
onPress={handleMpcOperations}
style={({ pressed }) => [
styles.buttonContainer,
pressed && styles.buttonPressed,
]}
>
<Text style={styles.buttonText}>Run MPC operations</Text>
</Pressable>

{mpcInfo.isPending && (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Operation in progress...</Text>
</View>
)}

{mpcInfo.keygen && (
<View style={styles.resultCard}>
<Text style={styles.cardTitle}>Key Generation</Text>
<View style={styles.dataRow}>
<Text style={styles.dataLabel}>Public Key:</Text>
<Text style={styles.dataValue}>
{mpcInfo.keygen.publicKeyHex}
</Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataLabel}>Time:</Text>
<Text style={styles.dataValue}>{mpcInfo.keygen.time}ms</Text>
</View>
</View>
)}

{mpcInfo.sign && (
<View style={styles.resultCard}>
<Text style={styles.cardTitle}>Signature Generation</Text>
<View style={styles.dataRow}>
<Text style={styles.dataLabel}>Signature:</Text>
<Text style={styles.dataValue}>
{mpcInfo.sign.signatureHex}
</Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataLabel}>Time:</Text>
<Text style={styles.dataValue}>{mpcInfo.sign.time}ms</Text>
</View>
</View>
)}
</>
)}
</View>
<StatusBar style="auto" />
</View>
);
}

const styles = StyleSheet.create({
appContainer: {
flex: 1,
backgroundColor: "#fff",
paddingVertical: 100,
paddingHorizontal: 20,
gap: 20,
},
title: {
fontSize: 20,
fontWeight: "bold",
textAlign: "center",
},
container: {
flex: 1,
justifyContent: "center",
},
buttonContainer: {
alignItems: "center",
backgroundColor: "#007bff",
borderRadius: 5,
marginHorizontal: 10,
padding: 15,
width: "100%",
},
buttonPressed: {
opacity: 0.8,
},
buttonText: {
color: "white",
fontWeight: "bold",
fontSize: 16,
},
loadingContainer: {
padding: 16,
alignItems: "center",
marginVertical: 8,
},
loadingText: {
color: "#666",
fontSize: 14,
fontStyle: "italic",
},
resultCard: {
backgroundColor: "white",
borderRadius: 12,
padding: 16,
marginVertical: 8,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
borderLeftWidth: 4,
borderLeftColor: "#007AFF",
},
cardTitle: {
fontSize: 16,
fontWeight: "bold",
color: "#333",
marginBottom: 12,
},
dataRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 8,
},
dataLabel: {
fontSize: 14,
fontWeight: "600",
color: "#666",
flex: 1,
},
dataValue: {
fontSize: 12,
color: "#333",
flex: 2,
textAlign: "right",
fontFamily: "Courier New",
lineHeight: 16,
},
});

Run the app

Let's see MPC in action! Make sure your development environment is properly set up for native builds.

Make sure native dependencies are installed:

# Generate the native iOS and Android directories
npx expo prebuild --clean

# Build and run the app on your preferred platform
npx expo run:android
# or
npx expo run:ios

Once the app launches:

  1. Tap the "Sign in with Google" button to authenticate
  2. After successful login, tap the "Run MPC operations" button
  3. Observe the key generation and signature results displayed on the screen