Skip to main content

cast/cmd/tempo/zone/
auth.rs

1//! Zone RPC auth wire format, mirrored from tempoxyz/zones at a1c15e9f.
2//!
3//! Source: <https://github.com/tempoxyz/zones/tree/a1c15e9f002a5efd150f56c5076fb35519288396/crates/rpc/src/auth/token.rs>
4
5use alloy_primitives::{B256, hex, keccak256};
6use alloy_signer::Signer;
7use eyre::Result;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10pub(super) const HEADER: &str = "x-authorization-token";
11
12/// Sign a short-lived token with the same raw-digest signature as `cast wallet sign --no-hash`.
13pub(super) async fn sign_token(
14    signer: &impl Signer,
15    zone_id: u32,
16    chain_id: u64,
17) -> Result<String> {
18    let issued = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
19    let (fields, digest) = token_fields(zone_id, chain_id, issued, issued + 600);
20    let signature = signer.sign_hash(&digest).await?;
21    let mut token = signature.as_bytes().to_vec();
22    token.extend_from_slice(&fields);
23    Ok(hex::encode(token))
24}
25
26fn token_fields(zone_id: u32, chain_id: u64, issued: u64, expires: u64) -> ([u8; 29], B256) {
27    let mut message = [0u8; 61];
28    message[..12].copy_from_slice(b"TempoZoneRPC");
29    message[33..37].copy_from_slice(&zone_id.to_be_bytes());
30    message[37..45].copy_from_slice(&chain_id.to_be_bytes());
31    message[45..53].copy_from_slice(&issued.to_be_bytes());
32    message[53..61].copy_from_slice(&expires.to_be_bytes());
33    (message[32..].try_into().unwrap(), keccak256(message))
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use alloy_primitives::Signature;
40    use alloy_signer_local::PrivateKeySigner;
41
42    #[tokio::test]
43    async fn token_recovers_wallet_from_raw_digest() {
44        let signer = PrivateKeySigner::random();
45        let token = hex::decode(sign_token(&signer, 42, 1337).await.unwrap()).unwrap();
46        assert_eq!(token.len(), 94);
47        let fields = &token[65..];
48        assert_eq!(&fields[..13], &hex::decode("000000002a0000000000000539").unwrap());
49        let issued = u64::from_be_bytes(fields[13..21].try_into().unwrap());
50        let expires = u64::from_be_bytes(fields[21..29].try_into().unwrap());
51        assert_eq!(expires - issued, 600);
52        let (_, digest) = token_fields(42, 1337, issued, expires);
53        let signature = Signature::try_from(&token[..65]).unwrap();
54        assert_eq!(signature.recover_address_from_prehash(&digest).unwrap(), signer.address());
55    }
56
57    #[test]
58    fn matches_zones_wire_vector() {
59        let (fields, digest) = token_fields(
60            0x0102_0304,
61            0x0506_0708_090a_0b0c,
62            0x0d0e_0f10_1112_1314,
63            0x1516_1718_191a_1b1c,
64        );
65        assert_eq!(
66            hex::encode(fields),
67            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c"
68        );
69        assert_eq!(
70            digest,
71            "0xf827387a933f40dfedece81ba4933feaef89e98a269f52f4f54dda2f1dac4171"
72                .parse::<B256>()
73                .unwrap()
74        );
75    }
76}