Skip to main content

clawdentity_core/runtime/
auth.rs

1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use ed25519_dalek::SigningKey;
4use getrandom::fill as getrandom_fill;
5
6use crate::error::{CoreError, Result};
7use crate::signing::{SignHttpRequestInput, sign_http_request};
8
9#[derive(Debug, Clone)]
10pub struct RelayConnectHeaders {
11    pub authorization: String,
12    pub signed_headers: Vec<(String, String)>,
13}
14
15/// TODO(clawdentity): document `build_relay_connect_headers`.
16pub fn build_relay_connect_headers(
17    relay_connect_url: &str,
18    ait: &str,
19    secret_key: &SigningKey,
20) -> Result<RelayConnectHeaders> {
21    let trimmed_ait = ait.trim();
22    if trimmed_ait.is_empty() {
23        return Err(CoreError::InvalidInput("AIT token is required".to_string()));
24    }
25
26    let parsed = url::Url::parse(relay_connect_url).map_err(|_| CoreError::InvalidUrl {
27        context: "relayConnectUrl",
28        value: relay_connect_url.to_string(),
29    })?;
30    let path_with_query = match parsed.query() {
31        Some(query) => format!("{}?{query}", parsed.path()),
32        None => parsed.path().to_string(),
33    };
34
35    let mut nonce_bytes = [0_u8; 16];
36    getrandom_fill(&mut nonce_bytes).map_err(|error| CoreError::InvalidInput(error.to_string()))?;
37    let nonce = URL_SAFE_NO_PAD.encode(nonce_bytes);
38    let timestamp = format!("{}", chrono::Utc::now().timestamp());
39    let signed = sign_http_request(&SignHttpRequestInput {
40        method: "GET",
41        path_with_query: &path_with_query,
42        timestamp: &timestamp,
43        nonce: &nonce,
44        body: &[],
45        secret_key,
46    })?;
47
48    Ok(RelayConnectHeaders {
49        authorization: format!("Claw {trimmed_ait}"),
50        signed_headers: signed.headers,
51    })
52}
53
54#[cfg(test)]
55mod tests {
56    use ed25519_dalek::SigningKey;
57
58    use super::build_relay_connect_headers;
59
60    #[test]
61    fn build_relay_headers_includes_authorization_and_claw_proof_headers() {
62        let key = SigningKey::from_bytes(&[7_u8; 32]);
63        let headers = build_relay_connect_headers(
64            "wss://proxy.clawdentity.com/v1/relay/connect",
65            "ait.jwt.value",
66            &key,
67        )
68        .expect("headers");
69        assert_eq!(headers.authorization, "Claw ait.jwt.value");
70        assert_eq!(headers.signed_headers.len(), 4);
71    }
72}