Skip to main content

trellis_client/
auth.rs

1use ed25519_dalek::{Signature, Signer, SigningKey};
2use serde_json::json;
3
4use crate::proof::{base64url_decode, base64url_encode, build_proof_input, sha256};
5use crate::TrellisClientError;
6
7/// Session-scoped signing material used for Trellis auth and RPC proofs.
8pub struct SessionAuth {
9    /// Public session key in base64url form.
10    pub session_key: String,
11    signing_key: SigningKey,
12}
13
14impl SessionAuth {
15    /// Construct a session authenticator from a base64url-encoded Ed25519 seed.
16    pub fn from_seed_base64url(seed_b64url: &str) -> Result<Self, TrellisClientError> {
17        let seed = base64url_decode(seed_b64url)?;
18        if seed.len() != 32 {
19            return Err(TrellisClientError::InvalidSeedLen(seed.len()));
20        }
21        let mut seed32 = [0u8; 32];
22        seed32.copy_from_slice(&seed);
23        let signing_key = SigningKey::from_bytes(&seed32);
24        let public = signing_key.verifying_key().to_bytes();
25        let session_key = base64url_encode(&public);
26        Ok(Self {
27            session_key,
28            signing_key,
29        })
30    }
31
32    /// Sign a domain-separated string value with `SHA-256(prefix:value)`.
33    pub fn sign_sha256_domain(&self, prefix: &str, value: &str) -> String {
34        let digest = sha256(format!("{prefix}:{value}").as_bytes());
35        let signature: Signature = self.signing_key.sign(&digest);
36        base64url_encode(&signature.to_bytes())
37    }
38
39    /// Create a service auth-callout token using an `iat` timestamp.
40    pub fn nats_connect_token(&self, iat: u64) -> String {
41        let signature = self.sign_sha256_domain("nats-connect", &iat.to_string());
42        serde_json::to_string(&json!({
43          "v": 1,
44          "sessionKey": self.session_key,
45          "iat": iat,
46          "sig": signature,
47        }))
48        .expect("nats auth token json")
49    }
50
51    /// Create a user auth-callout token using a binding token.
52    pub fn nats_connect_binding_token(&self, binding_token: &str) -> String {
53        let signature = self.sign_sha256_domain("nats-connect", binding_token);
54        serde_json::to_string(&json!({
55          "v": 1,
56          "sessionKey": self.session_key,
57          "bindingToken": binding_token,
58          "sig": signature,
59        }))
60        .expect("nats auth token json")
61    }
62
63    /// Return the inbox prefix derived from the session key.
64    pub fn inbox_prefix(&self) -> String {
65        format!(
66            "_INBOX.{}",
67            &self.session_key[..16.min(self.session_key.len())]
68        )
69    }
70
71    /// Create the `proof` header for a signed RPC request payload.
72    pub fn create_proof(&self, subject: &str, payload: &[u8]) -> String {
73        let payload_hash = sha256(payload);
74        let input = build_proof_input(&self.session_key, subject, &payload_hash);
75        let digest = sha256(&input);
76        let signature: Signature = self.signing_key.sign(&digest);
77        base64url_encode(&signature.to_bytes())
78    }
79}