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.rsfile, 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
- Full code example could be found here: https://github.com/silence-laboratories/m2m-relay-demo
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.
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
- Follow the React Native SDK setup guide to install dependencies and configure each mobile project.
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
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,
},
});