Skip to main content

agentmux_srv/backend/viewer/
client.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The paired-device side of the viewer: an HTTPS client that accepts exactly
5//! one certificate, by the fingerprint the pairing link carried
6//! (`cert::fingerprint`), the way a paired phone does. Used by Tower to read
7//! another AgentMux machine's processes (`backend::tower_peers`).
8//!
9//! Nothing about the certificate is trusted but its fingerprint: no chain, no
10//! name, no expiry. The TLS handshake signatures are still verified, so the
11//! peer must hold the certificate's private key.
12
13use std::sync::Arc;
14use std::time::Duration;
15
16use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
17use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
18use rustls::{DigitallySignedStruct, SignatureScheme};
19
20/// Accepts the one certificate whose fingerprint is `.0`.
21#[derive(Debug)]
22pub struct Pinned(pub String);
23
24impl ServerCertVerifier for Pinned {
25    fn verify_server_cert(
26        &self,
27        end_entity: &CertificateDer<'_>,
28        _intermediates: &[CertificateDer<'_>],
29        _server_name: &ServerName<'_>,
30        _ocsp: &[u8],
31        _now: UnixTime,
32    ) -> Result<ServerCertVerified, rustls::Error> {
33        let got = super::cert::fingerprint(end_entity);
34        if agentmux_common::secret_eq::secret_eq(got.as_bytes(), self.0.to_ascii_lowercase().as_bytes()) {
35            Ok(ServerCertVerified::assertion())
36        } else {
37            Err(rustls::Error::General("certificate fingerprint mismatch".into()))
38        }
39    }
40
41    fn verify_tls12_signature(
42        &self,
43        message: &[u8],
44        cert: &CertificateDer<'_>,
45        dss: &DigitallySignedStruct,
46    ) -> Result<HandshakeSignatureValid, rustls::Error> {
47        let algs = rustls::crypto::ring::default_provider().signature_verification_algorithms;
48        rustls::crypto::verify_tls12_signature(message, cert, dss, &algs)
49    }
50
51    fn verify_tls13_signature(
52        &self,
53        message: &[u8],
54        cert: &CertificateDer<'_>,
55        dss: &DigitallySignedStruct,
56    ) -> Result<HandshakeSignatureValid, rustls::Error> {
57        let algs = rustls::crypto::ring::default_provider().signature_verification_algorithms;
58        rustls::crypto::verify_tls13_signature(message, cert, dss, &algs)
59    }
60
61    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
62        rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes()
63    }
64}
65
66/// An HTTPS client that talks only to the server holding the certificate
67/// with `fingerprint`.
68pub fn pinned_client(fingerprint: &str, timeout: Duration) -> Result<reqwest::Client, String> {
69    let provider = Arc::new(rustls::crypto::ring::default_provider());
70    let config = rustls::ClientConfig::builder_with_provider(provider)
71        .with_safe_default_protocol_versions()
72        .map_err(|e| e.to_string())?
73        .dangerous()
74        .with_custom_certificate_verifier(Arc::new(Pinned(fingerprint.to_string())))
75        .with_no_client_auth();
76    reqwest::Client::builder()
77        .use_preconfigured_tls(config)
78        // Only ever a LAN address: straight there, never through a configured
79        // proxy (which usually can't reach it).
80        .no_proxy()
81        .timeout(timeout)
82        .build()
83        .map_err(|e| e.to_string())
84}