Skip to main content

erbridge_core/reverse/
enroll.rs

1//! Client side of enrollment: trade a single-use code for the credentials of a
2//! new tenant.
3//!
4//! The code is used as the Noise PSK, so the exchange is as private as any
5//! other tunnel connection. `serve` burns the code, creates the tenant, and
6//! sends the credentials as one frame inside the encrypted channel. If that
7//! frame is lost in transit the code is already spent; ask for a new one.
8
9use std::time::Duration;
10
11use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use tokio::net::TcpStream;
14
15use crate::noise;
16use crate::reverse::read_frame;
17
18const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
19const MAX_CREDENTIALS_LEN: usize = 4096;
20
21/// What an enrolled client needs to run `connect`: the token to authenticate
22/// with and the tunnel name `serve` will address it by.
23#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Credentials {
25    /// Tenant name; use it as the `[[connect.tunnel]]` name.
26    pub name: String,
27    /// Long-term secret; use it as the `connect` token. Shown once.
28    pub token: String,
29    /// The host this tenant is reached at.
30    pub sni: String,
31}
32
33impl std::fmt::Debug for Credentials {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("Credentials")
36            .field("name", &self.name)
37            .field("token", &"<redacted>")
38            .field("sni", &self.sni)
39            .finish()
40    }
41}
42
43impl Credentials {
44    pub fn to_toml(&self) -> Result<String> {
45        toml::to_string_pretty(self).context("serialising credentials")
46    }
47
48    pub fn from_toml(text: &str) -> Result<Self> {
49        toml::from_str(text).context("parsing credentials")
50    }
51}
52
53/// Redeems `code` at the control address `server`.
54pub async fn enroll(server: &str, code: &str) -> Result<Credentials> {
55    enroll_with_timeout(server, code, DEFAULT_TIMEOUT).await
56}
57
58/// [`enroll`] with an explicit overall time limit.
59pub async fn enroll_with_timeout(
60    server: &str,
61    code: &str,
62    timeout: Duration,
63) -> Result<Credentials> {
64    let attempt = async {
65        let tcp = TcpStream::connect(server)
66            .await
67            .with_context(|| format!("connecting to {server}"))?;
68        let _ = tcp.set_nodelay(true);
69        let mut stream = noise::connect(tcp, &noise::derive_psk(code))
70            .await
71            .context("enrollment handshake failed (is the code right?)")?;
72        let frame = read_frame(&mut stream, MAX_CREDENTIALS_LEN)
73            .await
74            .context("server did not return credentials")?;
75        serde_json::from_slice(&frame).context("server sent malformed credentials")
76    };
77    tokio::time::timeout(timeout, attempt)
78        .await
79        .context("timed out waiting for enrollment")?
80}