跳到主要内容

Relay Client Auth

Motivation

WalletConnect supports e2e encrypted messaging between multiple clients. Should a recipient be offline, WalletConnect stores the encrypted messages in the recipient's mailbox until they reconnect. The address of the mailbox is a unique sticky client_id that clients present when they connect to WalletConnect.

This document discusses the creation and usage of the client_id for authentication

Overview

WalletConnect expects an Authorization: Bearer <signed jwt> header when establishing the Websocket connection. Where websocket headers are not supported i.e. browsers, use url query param ?auth=<signed jwt>. Server should support both of these mechanisms.

The client_id is a DID of the public key for the key pair generated by the client when instantiating the SDK and persisted for the entire lifecyle.

Clients will generate a unique id per app intialization which will be used as a subject when signing a JWT with the client_id as the issuer and is persisted through duration of the app lifecycle.

Specification

Here we describe how a client can generate a Ed25519 key pair to authenticate itself using the public key as it's client id

Additionally we describe how we can construct and sign a JWT using a client-side generated unique id as subject

Finally we describe how the public key is encoded as did:key identifier which is used as client id.

API

// ---------- API ----------------------------------------------- //

function signJWT(subject: string, keyPair: ed25519.KeyPair): Promise<string>;
function verifyJWT(jwt: string): Promise<boolean>;

// ---------- Utilities ----------------------------------------------- //

function decodeJSON(str: string): any;
function encodeJSON(val: any): string;

function encodeIss(publicKey: Uint8Array): string;
function decodeIss(issuer: string): Uint8Array;

function encodeSig(bytes: Uint8Array): string;
function decodeSig(encoded: string): Uint8Array;

function encodeData(params: JWTData): string;
function decodeData(jwt: string): JWTData;

function encodeJWT(params: JWTSigned): string;
function decodeJWT(jwt: string): JWTSigned;

Reference Implementation

import * as ed25519 from "@stablelib/ed25519";
import { concat } from "uint8arrays/concat";
import { toString } from "uint8arrays/to-string";
import { fromString } from "uint8arrays/from-string";
import { fromMiliseconds } from "@walletconnect/time";
import { safeJsonParse, safeJsonStringify } from "@walletconnect/safe-json";

// ---------- Interfaces ----------------------------------------------- //

interface JWTHeader {
alg: "EdDSA";
typ: "JWT";
}

interface JWTPayload {
iss: string;
sub: string;
}

interface JWTData {
header: JWTHeader;
payload: JWTPayload;
}

interface JWTSigned extends JWTData {
signature: Uint8Array;
}

// ---------- Constants ----------------------------------------------- //

const JWT_ALG: JWTHeader["alg"] = "EdDSA";

const JWT_TYP: JWTHeader["typ"] = "JWT";

const JWT_DELIMITER = ".";

const JWT_ENCODING = "base64url";

const JSON_ENCODING = "utf8";

const DATA_ENCODING = "utf8";

const DID_DELIMITER = ":";

const DID_PREFIX = "did";

const DID_METHOD = "key";

const MULTICODEC_ED25519_ENCODING = "base58btc";

const MULTICODEC_ED25519_BASE = "z";

const MULTICODEC_ED25519_HEADER = "K36";

const MULTICODEC_ED25519_LENGTH = 32;

// ---------- JSON ----------------------------------------------- //

function decodeJSON(str: string): any {
return safeJsonParse(toString(fromString(str, JWT_ENCODING), JSON_ENCODING));
}

function encodeJSON(val: any): string {
return toString(
fromString(safeJsonStringify(val), JSON_ENCODING),
JWT_ENCODING
);
}

// ---------- Issuer ----------------------------------------------- //

function encodeIss(publicKey: Uint8Array): string {
const keyType = fromString(
MULTICODEC_ED25519_KEY_TYPE,
MULTICODEC_ED25519_ENCODING
);
const multicodec = toString(
concat([header, publicKey]),
MULTICODEC_ED25519_ENCODING
);
const multibase = MULTIBASE_BASE58BTC_PREFIX + multicodec;
return [DID_PREFIX, DID_METHOD, multibase].join(DID_DELIMITER);
}

function decodeIss(issuer: string): Uint8Array {
const [prefix, method, multibase] = issuer.split(DID_DELIMITER);
if (prefix !== DID_PREFIX || method !== DID_METHOD) {
throw new Error(`Issuer must be a DID with method "key"`);
}
const base = multibase.slice(0, 1);
if (base !== MULTIBASE_BASE58BTC_PREFIX) {
throw new Error(`Issuer must be a multibase with encoding base58btc`);
}
const multicodec = fromString(
multibase.slice(1),
MULTICODEC_ED25519_ENCODING
);
const keyType = toString(multicodec.slice(0, 2), MULTICODEC_ED25519_ENCODING);
if (keyType !== MULTICODEC_ED25519_KEY_TYPE) {
throw new Error(`Issuer must be a public key with type "Ed25519"`);
}
const publicKey = multicodec.slice(2);
if (publicKey.length !== MULTICODEC_ED25519_LENGTH) {
throw new Error(`Issuer must be a public key with length 32 bytes`);
}
return publicKey;
}

// ---------- Sig ----------------------------------------------- //

function encodeSig(bytes: Uint8Array): string {
return toString(bytes, JWT_ENCODING);
}

function decodeSig(encoded: string): Uint8Array {
return fromString(encoded, JWT_ENCODING);
}

// ---------- Data ----------------------------------------------- //

function encodeData(params: JWTData): Uint8Array {
return fromString(
[encodeJSON(params.header), encodeJSON(params.payload)].join(JWT_DELIMITER),
DATA_ENCODING
);
}

function decodeData(data: Uint8Array): JWTData {
const params = toString(data, DATA_ENCODING).split(JWT_DELIMITER);
const header = decodeJSON(params[0]);
const payload = decodeJSON(params[1]);
return { header, payload };
}

// ---------- JWT ----------------------------------------------- //

function encodeJWT(params: JWTSigned): string {
return [
encodeJSON(params.header),
encodeJSON(params.payload),
encodeSig(params.signature),
].join(JWT_DELIMITER);
}

function decodeJWT(jwt: string): JWTSigned {
const params = jwt.split(JWT_DELIMITER);
const header = decodeJSON(params[0]);
const payload = decodeJSON(params[1]);
const signature = decodeSig(params[2]);
return { header, payload, signature };
}

// ---------- API ----------------------------------------------- //

async function signJWT(
sub: string,
aud: string,
ttl: number,
keyPair: ed25519.KeyPair
) {
const header = { alg: JWT_ALG, typ: JWT_TYP };
const iss = encodeIss(keyPair.publicKey);
const iat = fromMiliseconds(Date.now());
const exp = iat + ttl;
const payload = { iss, sub, aud, iat, exp };
const data = encodeData({ header, payload });
const signature = ed25519.sign(keyPair.secretKey, data);
return encodeJWT({ header, payload, signature });
}

async function verifyJWT(jwt: string) {
const { header, payload, signature } = decodeJWT(jwt);
if (header.alg !== JWT_ALG || header.typ !== JWT_TYP) {
throw new Error("JWT must use EdDSA algorithm");
}
const publicKey = decodeIss(payload.iss);
const data = encodeData({ header, payload });
return ed25519.verify(publicKey, data, signature);
}

Test Cases

//  Client will sign a unique identifier as the subject
const sub =
"c479fe5dc464e771e78b193d239a65b58d278cad1c34bfb0b5716e5bb514928e";

// Client will include the server endpoint as audience
const aud = "wss://relay.walletconnect.com";

// Client will use the JWT for 24 hours
const ttl = 86400;

// Fixed seed to generate the same key pair
const seed = fromString(
"58e0254c211b858ef7896b00e3f36beeb13d568d47c6031c4218b87718061295",
"base16"
);

// Fixed issuedAt timestamp in seconds
const iat = 1656910097;

// Generate key pair from seed
const keyPair = ed25519.generateKeyPairFromSeed(seed);
// secretKey = "58e0254c211b858ef7896b00e3f36beeb13d568d47c6031c4218b87718061295884ab67f787b69e534bfdba8d5beb4e719700e90ac06317ed177d49e5a33be5a"
// publicKey = "884ab67f787b69e534bfdba8d5beb4e719700e90ac06317ed177d49e5a33be5a"

// Expected JWT for given payload
const expected = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvZEhad25lVlJTaHRhTGY4SktZa3hwREdwMXZHWm5wR21kQnBYOE0yZXh4SCIsInN1YiI6ImM0NzlmZTVkYzQ2NGU3NzFlNzhiMTkzZDIzOWE2NWI1OGQyNzhjYWQxYzM0YmZiMGI1NzE2ZTViYjUxNDkyOGUiLCJhdWQiOiJ3c3M6Ly9yZWxheS53YWxsZXRjb25uZWN0LmNvbSIsImlhdCI6MTY1NjkxMDA5NywiZXhwIjoxNjU2OTk2NDk3fQ.bAKl1swvwqqV_FgwvD4Bx3Yp987B9gTpZctyBviA-EkAuWc8iI8SyokOjkv9GJESgid4U8Tf2foCgrQp2qrxBA";

async function test() {
const jwt = await signJWT(sub, aud, ttl, keyPair);
console.log("jwt", jwt);
console.log("matches", jwt === expected);
const verified = await verifyJWT(jwt);
console.log("verified", verified);
}

test();