Skip to main content

Online message relay setup

Setup message relay server

The message relay server is a HTTP server that handled Websocket communication between 2-mobiles, which is written in Rust and built with Axum framework.

Step 0: Installation

  • Install Rust
  • Create Rust binary project.
  • Add prerequisites crates in your Cargo.toml:
[dependencies]
sl-mpc-mate = {version = "1.0.0-beta", features = ["simple-relay"]}
anyhow = "1"
axum = { version = "0.8.4", features = ["ws", "tokio", "http1"] }
futures-util = { version = "0.3.30", default-features = false }
tokio = { version = "1.43.0", features = [
"rt",
"rt-multi-thread",
"macros",
"signal",
] }
tokio-tungstenite = { version = "0.26" }
tower-http = { version = "0.6.2", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }

Step 1: Add module to handles Websocket message relay requests

  • Create src/relay_svc.rs file, then add below code:
use std::sync::Arc;

use axum::{
extract::{
ws::{Message, WebSocketUpgrade},
State,
},
response::Response,
};

use futures_util::{SinkExt, StreamExt};

use crate::Inner;

pub async fn handler(
State(state): State<Arc<Inner>>,
ws: WebSocketUpgrade,
) -> Response {
ws.on_upgrade(|mut socket| async move {
let mut conn = state.relay.connect();

loop {
tokio::select! {
Some(msg) = conn.next() => {
if socket.send(Message::binary(msg)).await.is_err() {
break;
}
}

msg = socket.recv() => {
match msg {
None => break,
Some(Ok(Message::Binary(msg))) => {
println!("relay recv binary msg: {:?}", msg.len());
let _ = conn.send(msg.to_vec()).await;
}

Some(Ok(Message::Close(_))) => {
tracing::debug!("recv close from the client");
break;
}

Some(Err(err)) => {
tracing::error!("recv error {err}");
break;
}

_ => {}
}
}
}
}

tracing::info!("close ws connection");
})
}

Step 2: Create main server app and add the route for message relay handler

  • In src/main.rs, add below code:
use std::{env, future::IntoFuture, net::ToSocketAddrs, sync::Arc};

use axum::{routing::get, Router};
use tokio::{
signal::unix::{signal, SignalKind},
task::JoinSet,
};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use sl_mpc_mate::coord::SimpleMessageRelay;
mod relay_svc;

struct Inner {
pub(crate) relay: SimpleMessageRelay,
}

#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
start_service().await?;
Ok(())
}

pub async fn start_service() -> anyhow::Result<()> {
let mut servers = JoinSet::new();
let state = {
let relay = SimpleMessageRelay::new();
Arc::new(Inner { relay })
};

let app = Router::new()
.route("/v1/msg-relay", get(relay_svc::handler))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.with_state(state);

for addrs in env::var("LISTEN")
.ok()
.unwrap_or_else(|| String::from(""))
.split(' ')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.chain(["localhost:8000"])
{
for addr in addrs.to_socket_addrs()? {
let listener = tokio::net::TcpListener::bind(addr).await?;

tracing::info!("listening on {}", listener.local_addr()?);

servers.spawn(
axum::serve(listener, app.clone().into_make_service())
.with_graceful_shutdown(shutdown())
.into_future(),
);
}
}

while servers.join_next().await.is_some() {}

Ok(())
}

async fn shutdown() {
let sigint = async {
signal(SignalKind::interrupt())
.expect("cant install SIGINT")
.recv()
.await;
};

let sigterm = async {
signal(SignalKind::terminate())
.expect("cant install SIGTERM")
.recv()
.await;
};

tokio::select! {
_ = sigint => {},
_ = sigterm => {},
};
}

Step 3: Run message relay server

cargo run

Setup the Mobile SDK (React Native)

  • Keep in mind, in this mobile to mobile setup, there will be one initiator phone, and other to follow up with the initiator request.
info

Before Mobile to mobile MPC operation the both parties needs to know each others message signer's public key and instance id.

For the prerequisites information exchange, we can use

  • QR code
  • Push notification
  • Through backend
  • Or any other out-of-band channels

Key generation

Step 0: Install React Native SDK for both mobiles

Step 1: Session creation

  • On the initiator (1st party):
const client = new CloudWebSocketClient("MESSAGE_RELAY_ENDPOINT", false);
const ms = await createMessageSigner();
const session = await createEcdsaM2MSession({
client: client,
messageSigner: ms1,
});
  • On the 2nd party, we have similar code:
const client = new CloudWebSocketClient("MESSAGE_RELAY_ENDPOINT", false);
const ms = await createMessageSigner();
const session1 = await createEcdsaM2MSession({
client: client,
messageSigner: ms,
});
What is message signer?

Silent Shard SDK secure communication between parties using the MessageSigner interface. Each party send message is signed with its own private key and other party could verify the message signature with the public key.

Currently Silent Shard SDK supports ED25519 and ECDSA_P256_SEC1 key types. We highly recommend, messageSigner generated in mobile TEE environment, which as iOS secure enclave or Android keystore. And persist through application lifecycle.

import { Ed25519 } from '@silencelaboratories/dkls-sdk';
import type { MessageSigner } from '@silencelaboratories/silent-shard-sdk';

export const createMessageSigner = async () => {
const ed = await Ed25519.create();
return {
keyType: 'ED25519',
publicKeyB64: ed.publicKeyB64,
sign(messageB64: string) {
return ed.sign(messageB64);
},
} as MessageSigner;
};

Step 2: Exchange instance id, verifying keys via QR code and authentication channel

warning

For simplicity for the guide, we use QR code and mock information exchange between 2 parties.

Step 3: Run Distributed Key Generation (DKG) on both sides

Now both parties have each over message signing public key, and instance id, we can start MPC key generation.

  • Finally run DKG on party 1:
const instanceId = await session.randomInstanceId();
showQRToParty2({instanceId, party1Vk: ms.publicKeyB64, keyType: ms.keyType}); // show QR code
const {party2Vk, keyType} = scanQrFromParty2(); // from QR code

const keyshare = await session.keygenP1({
instanceId,
otherVk: {
value: party2Vk,
keyType: keyType,
},
});
  • On party 2:
const {instanceId, party1Vk} = scanQrFromParty1(); // from QR code
const keyshare = await session.keygenP2({
instanceId,
otherVk: {
value: party1Vk,
keyType: keyType,
},
});

Signature generation

Step 0: We reuse sessions and party message verifying keys from previous steps

Step 1: Party 1 start signing process and share instance id with party 2

The both parties need to know the instance id for MPC signing generation.

  • On party 1:
const signInstanceId = await session.randomInstanceId();

showQRToParty2({signInstanceId}); // show QR code
  • On party 2:
const {signInstanceId} = scanQrFromParty1(); // from QR code

Step 2: Run Signature Generation on both sides

Now both parties have all required information to start MPC signature generation.

  • On party 1, we have:
const signature = await session.signgenP1({
instanceId: signInstanceId,
keyshare: keyshare,
messageHex: messageToSign, // sign over messageToSign
messageHash: "KECCAK256",
otherVk: {
value: party2Vk,
keyType: keyType,
},
});
  • On party 2:
const signature = await session2.signgenP2({
instanceId: signInstanceId,
keyshare: keyshare,
otherVk: {
value: party1Vk,
keyType: keyType,
},
});