Skip to main content

walletconnect_sdk/
relay_auth.rs

1/// Relay Auth
2///
3/// Utils to sign JWT for authorizing with Relay RPC
4///
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use alloy::hex;
8use base64ct::{Base64UrlUnpadded, Encoding};
9use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    constants::CRYPTO_JWT_TTL,
14    utils::{encode_iss, random_bytes32},
15};
16
17pub struct RelayAuth {
18    client_seed: [u8; 32],
19}
20
21impl RelayAuth {
22    pub fn new(client_seed: [u8; 32]) -> Self {
23        Self { client_seed }
24    }
25
26    // https://github.com/WalletConnect/walletconnect-monorepo/blob/1e618504de2c1802359ffec486120784c04bd240/packages/core/src/controllers/crypto.ts#L59
27    pub fn get_client_id(&self) -> String {
28        let seed = self.client_seed;
29        let key_pair = Keypair::from_seed(seed);
30        encode_iss(&key_pair.public_key)
31    }
32
33    // https://github.com/WalletConnect/walletconnect-monorepo/blob/b39a5d4e62f5517ef47a70b5b93f27585b7132e8/packages/core/src/controllers/crypto.ts#L73
34    pub fn sign_jwt(&self, aud: &str) -> String {
35        let keypair = Keypair::from_seed(self.client_seed);
36
37        let sub = random_bytes32(); // randomSessionIdentifier;
38        let ttl = CRYPTO_JWT_TTL;
39        sign_jwt(&hex::encode(sub), aud, ttl, &keypair, None)
40    }
41}
42
43// Only used for JWT signing, not used for encryption
44#[derive(Debug, Clone)]
45pub struct Keypair {
46    pub seed: [u8; 32],
47    pub secret_key: [u8; 64],
48    pub public_key: [u8; 32],
49}
50
51impl Keypair {
52    pub fn generate() -> Self {
53        Keypair::from_seed(random_bytes32())
54    }
55
56    pub fn sign(&self, data: &[u8]) -> [u8; 64] {
57        let mut signing_key = SigningKey::from(self.seed);
58        signing_key.sign(data).to_bytes()
59    }
60
61    pub fn from_bytes64_secret(secret_key: [u8; 64]) -> Self {
62        let mut seed = [0u8; 32];
63        let mut public_key = [0u8; 32];
64
65        seed.copy_from_slice(&secret_key[..32]);
66        public_key.copy_from_slice(&secret_key[32..]);
67
68        let signing_key = SigningKey::from_bytes(&seed);
69        let calculated_public_key = signing_key.verifying_key().to_bytes();
70
71        assert_eq!(
72            public_key, calculated_public_key,
73            "Public key does not match the signing key"
74        );
75
76        Keypair {
77            seed,
78            secret_key,
79            public_key,
80        }
81    }
82
83    // https://github.com/WalletConnect/walletconnect-utils/blob/4d8eb66bcea89857c630017774845e872a66922a/relay/relay-auth/src/api.ts#L14
84    pub fn from_seed(seed: [u8; 32]) -> Keypair {
85        let signing_key = SigningKey::from_bytes(&seed);
86        let public_key = signing_key.verifying_key().to_bytes();
87
88        let mut secret_key = [0u8; 64];
89
90        secret_key[..32].copy_from_slice(&seed);
91        secret_key[32..].copy_from_slice(&public_key);
92
93        Keypair {
94            seed,
95            secret_key,
96            public_key,
97        }
98    }
99}
100
101#[derive(Serialize, Deserialize)]
102pub struct IridiumJWTHeader {
103    pub alg: &'static str,
104    pub typ: &'static str,
105}
106
107#[derive(Serialize, Deserialize)]
108pub struct IridiumJWTPayload {
109    pub iss: String,
110    pub sub: String,
111    pub aud: String,
112    pub iat: u64,
113    pub exp: u64,
114}
115
116#[derive(Serialize)]
117pub struct IridiumJWTSigned<'a> {
118    pub header: &'a IridiumJWTHeader,
119    pub payload: &'a IridiumJWTPayload,
120    pub signature: String,
121}
122
123fn encode_json<T: ?Sized + Serialize>(val: &T) -> String {
124    Base64UrlUnpadded::encode_string(
125        serde_json::to_string(val).unwrap().as_bytes(),
126    )
127}
128
129fn encode_data(
130    header: &IridiumJWTHeader,
131    payload: &IridiumJWTPayload,
132) -> (Vec<u8>, String) {
133    let h = encode_json(header);
134    let p = encode_json(payload);
135    let joined = format!("{h}.{p}");
136    (joined.as_bytes().to_vec(), joined)
137}
138
139pub fn sign_jwt(
140    sub: &str,
141    aud: &str,
142    ttl: u64,
143    keypair: &Keypair,
144    iat_opt: Option<u64>,
145) -> String {
146    let iat = iat_opt.unwrap_or_else(|| {
147        SystemTime::now()
148            .duration_since(UNIX_EPOCH)
149            .unwrap()
150            .as_secs()
151    });
152    let exp = iat + ttl;
153
154    let header = IridiumJWTHeader {
155        alg: "EdDSA",
156        typ: "JWT",
157    };
158    let iss = encode_iss(&keypair.public_key);
159    let payload = IridiumJWTPayload {
160        iss,
161        sub: sub.to_string(),
162        aud: aud.to_string(),
163        iat,
164        exp,
165    };
166
167    let (data, jwt_head_payload) = encode_data(&header, &payload);
168
169    let signature = keypair.sign(&data);
170    let sig_encoded = Base64UrlUnpadded::encode_string(&signature);
171
172    format!("{jwt_head_payload}.{sig_encoded}")
173}
174
175#[cfg(test)]
176mod test {
177    use super::RelayAuth;
178
179    #[test]
180    fn test_1() {
181        let wk = RelayAuth::new([0; 32]);
182        let client_id = wk.get_client_id();
183        assert_eq!(
184            client_id,
185            "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
186        );
187    }
188
189    #[test]
190    fn test_2() {
191        let wk = RelayAuth::new([
192            23, 113, 199, 94, 246, 41, 119, 10, 250, 248, 253, 136, 173, 241,
193            191, 149, 165, 249, 17, 42, 46, 189, 120, 175, 78, 88, 53, 83, 254,
194            16, 32, 150,
195        ]);
196        let client_id = wk.get_client_id();
197        assert_eq!(
198            client_id,
199            "did:key:z6MkriJMhx6cLMiwwfuJ3NCGw8C8UjB9KoVHB7QSBaBxMx3y"
200        );
201    }
202}