Skip to main content

rightkit_send/
net.rs

1//! Transport for the LocalSend protocol: a self-signed TLS identity, rustls
2//! streams, and the small slice of HTTP/1.1 the protocol needs. Every
3//! connection carries one request and is closed after the response.
4
5use super::proto;
6use rustls::DistinguishedName;
7use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
8use rustls::crypto::{
9    CryptoProvider, WebPkiSupportedAlgorithms, verify_tls12_signature, verify_tls13_signature,
10};
11use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
12use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
13use rustls::{
14    ClientConfig, ClientConnection, DigitallySignedStruct, ServerConfig, ServerConnection,
15    SignatureScheme, StreamOwned,
16};
17use sha2::{Digest, Sha256};
18use std::io::{self, BufRead, BufReader, Read, Write};
19use std::net::{IpAddr, Shutdown, SocketAddr, TcpStream};
20use std::sync::{Arc, Mutex};
21use std::time::Duration;
22
23/// Whether `fingerprint` is a SHA-256 in hex, the only thing a send is pinned to.
24pub fn valid_pin(fingerprint: &str) -> bool {
25    fingerprint.len() == 64 && fingerprint.bytes().all(|b| b.is_ascii_hexdigit())
26}
27
28pub fn sha256_hex(bytes: &[u8]) -> String {
29    Sha256::digest(bytes)
30        .iter()
31        .map(|b| format!("{b:02x}"))
32        .collect()
33}
34
35fn other<E: std::fmt::Display>(error: E) -> io::Error {
36    io::Error::other(error.to_string())
37}
38
39fn provider() -> Arc<CryptoProvider> {
40    Arc::new(rustls::crypto::ring::default_provider())
41}
42
43// ---- identity --------------------------------------------------------------
44
45/// This device's certificate and key. The fingerprint the protocol asks for is
46/// the SHA-256 of the certificate (DER), as lowercase hex. The host stores the
47/// two DER blobs (a Keychain item, or a file it protects) and hands them back
48/// through `Identity::load`; the crate never writes key material.
49#[derive(Clone)]
50pub struct Identity {
51    pub cert_der: Vec<u8>,
52    pub key_der: Vec<u8>,
53    pub fingerprint: String,
54}
55
56impl std::fmt::Debug for Identity {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("Identity")
59            .field("fingerprint", &self.fingerprint)
60            .finish_non_exhaustive()
61    }
62}
63
64impl Identity {
65    /// A new identity: a fresh P-256 key and a self-signed certificate whose
66    /// common name is `alias` (control characters removed, at most 64 characters).
67    /// The host must store both `cert_der` and `key_der` and reuse them on every
68    /// start. A new identity has a new fingerprint, so other devices treat it as
69    /// a stranger.
70    pub fn generate(alias: &str) -> io::Result<Identity> {
71        let key_pair = rcgen::KeyPair::generate().map_err(other)?;
72        let common_name: String = alias.chars().filter(|c| !c.is_control()).take(64).collect();
73        let mut params =
74            rcgen::CertificateParams::new(vec!["localsend".to_string()]).map_err(other)?;
75        params.distinguished_name.push(
76            rcgen::DnType::CommonName,
77            rcgen::DnValue::Utf8String(common_name),
78        );
79        let certificate = params.self_signed(&key_pair).map_err(other)?;
80        Identity::from_der(certificate.der().to_vec(), key_pair.serialize_der())
81    }
82
83    /// The identity made from a stored certificate (DER) and its PKCS#8 private
84    /// key (DER). Fails when the pair cannot serve TLS: corrupt bytes, or a key
85    /// that does not belong to the certificate.
86    pub fn load(cert_der: Vec<u8>, key_der: Vec<u8>) -> io::Result<Identity> {
87        Identity::from_der(cert_der, key_der)
88    }
89
90    fn from_der(cert_der: Vec<u8>, key_der: Vec<u8>) -> io::Result<Identity> {
91        let identity = Identity {
92            fingerprint: sha256_hex(&cert_der),
93            cert_der,
94            key_der,
95        };
96        server_config(&identity)?;
97        Ok(identity)
98    }
99}
100
101/// The User-Agent header sent on every outgoing request, set by the service.
102static USER_AGENT: Mutex<String> = Mutex::new(String::new());
103
104pub fn set_user_agent(agent: &str) {
105    *USER_AGENT
106        .lock()
107        .unwrap_or_else(|poisoned| poisoned.into_inner()) = agent.to_string();
108}
109
110fn user_agent() -> String {
111    USER_AGENT
112        .lock()
113        .unwrap_or_else(|poisoned| poisoned.into_inner())
114        .clone()
115}
116
117/// The text rustls gives when a peer's certificate is not the one its
118/// fingerprint names (set by `PinVerifier`). Used to classify the failure.
119pub(crate) const MISMATCH_MESSAGE: &str = "the device's certificate does not match its fingerprint";
120
121/// Whether an I/O error from a connection is the fingerprint mismatch that
122/// `PinVerifier` raises, as opposed to any other failure.
123pub(crate) fn is_identity_mismatch(error: &io::Error) -> bool {
124    error
125        .get_ref()
126        .and_then(|inner| inner.downcast_ref::<rustls::Error>())
127        .is_some_and(|tls| matches!(tls, rustls::Error::General(text) if text == MISMATCH_MESSAGE))
128}
129
130/// Asks a connecting device for its certificate without requiring one. Other
131/// LocalSend apps send none; Pulse sends its own. Any self-signed certificate
132/// is taken, but the handshake signature must be made by its key, so a device
133/// that presents one has proved it holds the key (`Wire::peer_fingerprint`
134/// then names it). The caller compares the fingerprint with the claimed one.
135#[derive(Debug)]
136struct OptionalClientCert {
137    algorithms: WebPkiSupportedAlgorithms,
138}
139
140impl ClientCertVerifier for OptionalClientCert {
141    fn offer_client_auth(&self) -> bool {
142        true
143    }
144
145    fn client_auth_mandatory(&self) -> bool {
146        false
147    }
148
149    fn root_hint_subjects(&self) -> &[DistinguishedName] {
150        &[]
151    }
152
153    fn verify_client_cert(
154        &self,
155        _end_entity: &CertificateDer<'_>,
156        _intermediates: &[CertificateDer<'_>],
157        _now: UnixTime,
158    ) -> Result<ClientCertVerified, rustls::Error> {
159        Ok(ClientCertVerified::assertion())
160    }
161
162    fn verify_tls12_signature(
163        &self,
164        message: &[u8],
165        cert: &CertificateDer<'_>,
166        dss: &DigitallySignedStruct,
167    ) -> Result<HandshakeSignatureValid, rustls::Error> {
168        verify_tls12_signature(message, cert, dss, &self.algorithms)
169    }
170
171    fn verify_tls13_signature(
172        &self,
173        message: &[u8],
174        cert: &CertificateDer<'_>,
175        dss: &DigitallySignedStruct,
176    ) -> Result<HandshakeSignatureValid, rustls::Error> {
177        verify_tls13_signature(message, cert, dss, &self.algorithms)
178    }
179
180    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
181        self.algorithms.supported_schemes()
182    }
183}
184
185/// The certificate and key this process presents as a client, set by the
186/// running service so every connection it makes proves its identity.
187static CLIENT_IDENTITY: Mutex<Option<(Vec<u8>, Vec<u8>)>> = Mutex::new(None);
188
189pub fn set_client_identity(identity: &Identity) {
190    let mut slot = CLIENT_IDENTITY
191        .lock()
192        .unwrap_or_else(|poisoned| poisoned.into_inner());
193    *slot = Some((identity.cert_der.clone(), identity.key_der.clone()));
194}
195
196pub fn server_config(identity: &Identity) -> io::Result<Arc<ServerConfig>> {
197    let certificate = CertificateDer::from(identity.cert_der.clone());
198    let key = PrivateKeyDer::from(PrivatePkcs8KeyDer::from(identity.key_der.clone()));
199    let provider = provider();
200    let verifier = OptionalClientCert {
201        algorithms: provider.signature_verification_algorithms,
202    };
203    let config = ServerConfig::builder_with_provider(provider)
204        .with_safe_default_protocol_versions()
205        .map_err(other)?
206        .with_client_cert_verifier(Arc::new(verifier))
207        .with_single_cert(vec![certificate], key)
208        .map_err(other)?;
209    Ok(Arc::new(config))
210}
211
212/// LocalSend devices use self-signed certificates and do not validate a chain.
213/// Like them, this accepts any certificate, but when the peer's announced
214/// fingerprint is a SHA-256 it must match the certificate shown, so a different
215/// device answering on a remembered address is refused. The handshake signature
216/// is checked against that certificate's key, so only a holder of the private
217/// key passes; copying the public certificate is not enough.
218///
219/// Unverified interop risk: the check parses the certificate with webpki, which
220/// is stricter than some LocalSend apps' certificate generators (RSA, older
221/// X.509 profiles). A peer whose certificate it rejects cannot be sent to.
222#[derive(Debug)]
223struct PinVerifier {
224    expected: Option<String>,
225    algorithms: WebPkiSupportedAlgorithms,
226}
227
228impl ServerCertVerifier for PinVerifier {
229    fn verify_server_cert(
230        &self,
231        end_entity: &CertificateDer<'_>,
232        _intermediates: &[CertificateDer<'_>],
233        _server_name: &ServerName<'_>,
234        _ocsp_response: &[u8],
235        _now: UnixTime,
236    ) -> Result<ServerCertVerified, rustls::Error> {
237        if let Some(expected) = &self.expected
238            && sha256_hex(end_entity.as_ref()) != *expected
239        {
240            return Err(rustls::Error::General(
241                "the device's certificate does not match its fingerprint".into(),
242            ));
243        }
244        Ok(ServerCertVerified::assertion())
245    }
246
247    fn verify_tls12_signature(
248        &self,
249        message: &[u8],
250        cert: &CertificateDer<'_>,
251        dss: &DigitallySignedStruct,
252    ) -> Result<HandshakeSignatureValid, rustls::Error> {
253        verify_tls12_signature(message, cert, dss, &self.algorithms)
254    }
255
256    fn verify_tls13_signature(
257        &self,
258        message: &[u8],
259        cert: &CertificateDer<'_>,
260        dss: &DigitallySignedStruct,
261    ) -> Result<HandshakeSignatureValid, rustls::Error> {
262        verify_tls13_signature(message, cert, dss, &self.algorithms)
263    }
264
265    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
266        self.algorithms.supported_schemes()
267    }
268}
269
270fn client_config(fingerprint: &str) -> io::Result<Arc<ClientConfig>> {
271    let provider = provider();
272    let expected = {
273        let f = fingerprint.to_ascii_lowercase();
274        (f.len() == 64 && f.bytes().all(|b| b.is_ascii_hexdigit())).then_some(f)
275    };
276    let verifier = PinVerifier {
277        expected,
278        algorithms: provider.signature_verification_algorithms,
279    };
280    let builder = ClientConfig::builder_with_provider(provider)
281        .with_safe_default_protocol_versions()
282        .map_err(other)?
283        .dangerous()
284        .with_custom_certificate_verifier(Arc::new(verifier));
285    let own = CLIENT_IDENTITY
286        .lock()
287        .unwrap_or_else(|poisoned| poisoned.into_inner())
288        .clone();
289    let config = match own {
290        Some((cert, key)) => builder
291            .with_client_auth_cert(
292                vec![CertificateDer::from(cert)],
293                PrivateKeyDer::from(PrivatePkcs8KeyDer::from(key)),
294            )
295            .map_err(other)?,
296        None => builder.with_no_client_auth(),
297    };
298    Ok(Arc::new(config))
299}
300
301// ---- streams ---------------------------------------------------------------
302
303pub enum Wire {
304    Plain(TcpStream),
305    Server(Box<StreamOwned<ServerConnection, TcpStream>>),
306    Client(Box<StreamOwned<ClientConnection, TcpStream>>),
307}
308
309impl Read for Wire {
310    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
311        match self {
312            Wire::Plain(s) => s.read(buf),
313            Wire::Server(s) => s.read(buf),
314            Wire::Client(s) => s.read(buf),
315        }
316    }
317}
318
319impl Write for Wire {
320    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
321        match self {
322            Wire::Plain(s) => s.write(buf),
323            Wire::Server(s) => s.write(buf),
324            Wire::Client(s) => s.write(buf),
325        }
326    }
327    fn flush(&mut self) -> io::Result<()> {
328        match self {
329            Wire::Plain(s) => s.flush(),
330            Wire::Server(s) => s.flush(),
331            Wire::Client(s) => s.flush(),
332        }
333    }
334}
335
336impl Wire {
337    pub fn tcp(&self) -> &TcpStream {
338        match self {
339            Wire::Plain(s) => s,
340            Wire::Server(s) => &s.sock,
341            Wire::Client(s) => &s.sock,
342        }
343    }
344
345    /// SHA-256 of the certificate the peer presented in the handshake, when it
346    /// presented one (server side). The handshake has already checked that the
347    /// peer holds the matching key.
348    pub fn peer_fingerprint(&self) -> Option<String> {
349        match self {
350            Wire::Server(s) => s
351                .conn
352                .peer_certificates()
353                .and_then(|certs| certs.first())
354                .map(|cert| sha256_hex(cert.as_ref())),
355            _ => None,
356        }
357    }
358
359    /// Say goodbye (TLS close_notify) and close the socket.
360    pub fn finish(&mut self) {
361        match self {
362            Wire::Server(s) => s.conn.send_close_notify(),
363            Wire::Client(s) => s.conn.send_close_notify(),
364            Wire::Plain(_) => {}
365        }
366        let _ = self.flush();
367        let _ = self.tcp().shutdown(Shutdown::Both);
368    }
369}
370
371/// Wrap an accepted connection in TLS (server side).
372pub fn accept_tls(config: &Arc<ServerConfig>, socket: TcpStream) -> io::Result<Wire> {
373    let connection = ServerConnection::new(config.clone()).map_err(other)?;
374    Ok(Wire::Server(Box::new(StreamOwned::new(connection, socket))))
375}
376
377/// Connect to a peer. HTTPS peers are pinned to their announced fingerprint.
378pub fn connect(
379    ip: IpAddr,
380    port: u16,
381    https: bool,
382    fingerprint: &str,
383    read_timeout: Duration,
384) -> io::Result<Wire> {
385    connect_within(
386        ip,
387        port,
388        https,
389        fingerprint,
390        Duration::from_secs(5),
391        read_timeout,
392    )
393}
394
395/// `connect` with its own limit on how long the TCP connection may take.
396pub fn connect_within(
397    ip: IpAddr,
398    port: u16,
399    https: bool,
400    fingerprint: &str,
401    connect_timeout: Duration,
402    read_timeout: Duration,
403) -> io::Result<Wire> {
404    let socket = TcpStream::connect_timeout(&SocketAddr::new(ip, port), connect_timeout)?;
405    socket.set_read_timeout(Some(read_timeout))?;
406    socket.set_write_timeout(Some(Duration::from_secs(30)))?;
407    let _ = socket.set_nodelay(true);
408    if !https {
409        return Ok(Wire::Plain(socket));
410    }
411    let config = client_config(fingerprint)?;
412    let name = ServerName::IpAddress(ip.into());
413    let connection = ClientConnection::new(config, name).map_err(other)?;
414    Ok(Wire::Client(Box::new(StreamOwned::new(connection, socket))))
415}
416
417// ---- HTTP: requests (server side) -----------------------------------------
418
419pub struct Request {
420    pub method: String,
421    pub path: String,
422    pub query: Vec<(String, String)>,
423    pub headers: Vec<(String, String)>,
424}
425
426impl Request {
427    pub fn header(&self, name: &str) -> Option<&str> {
428        self.headers
429            .iter()
430            .find(|(k, _)| k.eq_ignore_ascii_case(name))
431            .map(|(_, v)| v.as_str())
432    }
433    pub fn param(&self, name: &str) -> Option<&str> {
434        self.query
435            .iter()
436            .find(|(k, _)| k == name)
437            .map(|(_, v)| v.as_str())
438    }
439}
440
441fn header_of<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
442    headers
443        .iter()
444        .find(|(k, _)| k.eq_ignore_ascii_case(name))
445        .map(|(_, v)| v.as_str())
446}
447
448fn bad(message: &str) -> io::Error {
449    io::Error::new(io::ErrorKind::InvalidData, message.to_string())
450}
451
452fn read_line_limited<R: BufRead>(reader: &mut R, limit: u64) -> io::Result<String> {
453    let mut line = String::new();
454    let read = reader.by_ref().take(limit).read_line(&mut line)?;
455    if read == 0 {
456        return Err(io::Error::new(
457            io::ErrorKind::UnexpectedEof,
458            "connection closed",
459        ));
460    }
461    if !line.ends_with('\n') {
462        return Err(bad("line too long"));
463    }
464    Ok(line.trim_end_matches(['\r', '\n']).to_string())
465}
466
467fn read_headers<R: BufRead>(reader: &mut R) -> io::Result<Vec<(String, String)>> {
468    let mut headers = Vec::new();
469    loop {
470        let line = read_line_limited(reader, 16 * 1024)?;
471        if line.is_empty() {
472            return Ok(headers);
473        }
474        if headers.len() >= 100 {
475            return Err(bad("too many headers"));
476        }
477        if let Some((name, value)) = line.split_once(':') {
478            headers.push((name.trim().to_string(), value.trim().to_string()));
479        }
480    }
481}
482
483pub fn read_request<R: BufRead>(reader: &mut R) -> io::Result<Request> {
484    let line = read_line_limited(reader, 16 * 1024)?;
485    let mut parts = line.split_whitespace();
486    let method = parts
487        .next()
488        .ok_or_else(|| bad("empty request"))?
489        .to_string();
490    let target = parts.next().ok_or_else(|| bad("no request target"))?;
491    let (path, query_text) = target.split_once('?').unwrap_or((target, ""));
492    let query = query_text
493        .split('&')
494        .filter(|p| !p.is_empty())
495        .map(|pair| {
496            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
497            (proto::url_decode(k), proto::url_decode(v))
498        })
499        .collect();
500    let headers = read_headers(reader)?;
501    Ok(Request {
502        method,
503        path: path.to_string(),
504        query,
505        headers,
506    })
507}
508
509/// Read a message body (Content-Length or chunked) and hand it to `sink` in
510/// pieces. More than `max` bytes is an error.
511pub fn read_body<R: BufRead>(
512    reader: &mut R,
513    headers: &[(String, String)],
514    max: u64,
515    sink: &mut dyn FnMut(&[u8]) -> io::Result<()>,
516) -> io::Result<u64> {
517    let find = |name: &str| header_of(headers, name);
518    let mut buffer = vec![0u8; 64 * 1024];
519    let mut total = 0u64;
520    let chunked =
521        find("transfer-encoding").is_some_and(|v| v.to_ascii_lowercase().contains("chunked"));
522    if chunked {
523        loop {
524            let size_line = read_line_limited(reader, 1024)?;
525            let size_text = size_line.split(';').next().unwrap_or("").trim();
526            let mut remaining =
527                u64::from_str_radix(size_text, 16).map_err(|_| bad("bad chunk size"))?;
528            if remaining == 0 {
529                while !read_line_limited(reader, 16 * 1024)?.is_empty() {}
530                return Ok(total);
531            }
532            while remaining > 0 {
533                let want = remaining.min(buffer.len() as u64) as usize;
534                let n = reader.read(&mut buffer[..want])?;
535                if n == 0 {
536                    return Err(io::Error::new(
537                        io::ErrorKind::UnexpectedEof,
538                        "body cut short",
539                    ));
540                }
541                total += n as u64;
542                if total > max {
543                    return Err(bad("body too large"));
544                }
545                sink(&buffer[..n])?;
546                remaining -= n as u64;
547            }
548            read_line_limited(reader, 16)?; // the CRLF after the chunk
549        }
550    }
551    let length = match find("content-length") {
552        Some(v) => v.parse::<u64>().map_err(|_| bad("bad content length"))?,
553        None => return Ok(0),
554    };
555    if length > max {
556        return Err(bad("body too large"));
557    }
558    let mut remaining = length;
559    while remaining > 0 {
560        let want = remaining.min(buffer.len() as u64) as usize;
561        let n = reader.read(&mut buffer[..want])?;
562        if n == 0 {
563            return Err(io::Error::new(
564                io::ErrorKind::UnexpectedEof,
565                "body cut short",
566            ));
567        }
568        total += n as u64;
569        sink(&buffer[..n])?;
570        remaining -= n as u64;
571    }
572    Ok(total)
573}
574
575fn reason(status: u16) -> &'static str {
576    match status {
577        200 => "OK",
578        204 => "No Content",
579        400 => "Bad Request",
580        401 => "Unauthorized",
581        403 => "Forbidden",
582        404 => "Not Found",
583        409 => "Conflict",
584        413 => "Payload Too Large",
585        422 => "Unprocessable Entity",
586        429 => "Too Many Requests",
587        _ => "Internal Server Error",
588    }
589}
590
591pub fn write_response<W: Write>(
592    writer: &mut W,
593    status: u16,
594    json: Option<&[u8]>,
595) -> io::Result<()> {
596    let body = json.unwrap_or(&[]);
597    let mut head = format!(
598        "HTTP/1.1 {status} {}\r\nConnection: close\r\n",
599        reason(status)
600    );
601    if json.is_some() {
602        head.push_str("Content-Type: application/json\r\n");
603    }
604    head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
605    writer.write_all(head.as_bytes())?;
606    writer.write_all(body)?;
607    writer.flush()
608}
609
610// ---- HTTP: requests (client side) -----------------------------------------
611
612pub struct Reply {
613    pub status: u16,
614    pub body: Vec<u8>,
615}
616
617/// Send one request on `wire` and read the whole response. `write_body` writes
618/// exactly `length` bytes.
619pub fn call(
620    wire: &mut Wire,
621    method: &str,
622    host: &str,
623    target: &str,
624    content_type: Option<&str>,
625    length: u64,
626    write_body: &mut dyn FnMut(&mut Wire) -> io::Result<()>,
627) -> io::Result<Reply> {
628    let agent = user_agent();
629    let mut head = format!(
630        "{method} {target} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: {agent}\r\nConnection: close\r\nContent-Length: {length}\r\n"
631    );
632    if let Some(kind) = content_type {
633        head.push_str(&format!("Content-Type: {kind}\r\n"));
634    }
635    head.push_str("\r\n");
636    wire.write_all(head.as_bytes())?;
637    write_body(wire)?;
638    wire.flush()?;
639
640    let mut reader = BufReader::new(&mut *wire);
641    let status_line = read_line_limited(&mut reader, 16 * 1024)?;
642    let status: u16 = status_line
643        .split_whitespace()
644        .nth(1)
645        .and_then(|s| s.parse().ok())
646        .ok_or_else(|| bad("bad status line"))?;
647    let headers = read_headers(&mut reader)?;
648    let mut body = Vec::new();
649    let has_length = headers
650        .iter()
651        .any(|(k, _)| k.eq_ignore_ascii_case("content-length"));
652    let chunked = headers.iter().any(|(k, v)| {
653        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
654    });
655    const CAP: u64 = 8 * 1024 * 1024;
656    if has_length || chunked {
657        read_body(&mut reader, &headers, CAP, &mut |c| {
658            body.extend_from_slice(c);
659            Ok(())
660        })?;
661    } else {
662        // No length: the body runs until the peer closes. A close without a TLS
663        // goodbye is a normal end here.
664        let mut limited = (&mut reader).take(CAP);
665        match limited.read_to_end(&mut body) {
666            Ok(_) => {}
667            Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {}
668            Err(e) => return Err(e),
669        }
670    }
671    Ok(Reply { status, body })
672}
673
674/// One small request with an optional JSON body, on a fresh connection.
675#[allow(clippy::too_many_arguments)]
676pub fn request_json(
677    ip: IpAddr,
678    port: u16,
679    https: bool,
680    fingerprint: &str,
681    method: &str,
682    target: &str,
683    json: Option<&[u8]>,
684    timeout: Duration,
685) -> io::Result<Reply> {
686    let mut wire = connect(ip, port, https, fingerprint, timeout)?;
687    let payload = json.unwrap_or(&[]);
688    let host = format!("{ip}:{port}");
689    let result = call(
690        &mut wire,
691        method,
692        &host,
693        target,
694        json.map(|_| "application/json"),
695        payload.len() as u64,
696        &mut |w| w.write_all(payload),
697    );
698    wire.finish();
699    result
700}
701
702/// Connect-back proof for a sender that presented no client certificate: open
703/// our own TLS connection to `ip:port` and require that the server there holds
704/// the private key of a certificate whose SHA-256 is `fingerprint` (the pinned
705/// handshake signature is genuinely verified, see `PinVerifier`). False for a
706/// malformed fingerprint, a refused connection, a wrong key or a timeout (3 s).
707///
708/// Residual limit: this proves a holder of the key answers at that address, not
709/// that it is the one now calling. An attacker who relays the handshake to the
710/// real device while it is online on the same network could still pass.
711/// Even a plain TCP forwarder passes it, so it is a convenience tier, never
712/// identity. Certificate proof (`Wire::peer_fingerprint`) has no such limit.
713pub fn prove_key(ip: IpAddr, port: u16, fingerprint: &str) -> bool {
714    let fingerprint = fingerprint.to_ascii_lowercase();
715    if !valid_pin(&fingerprint) {
716        return false;
717    }
718    let timeout = Duration::from_secs(3);
719    let Ok(mut wire) = connect_within(ip, port, true, &fingerprint, timeout, timeout) else {
720        return false;
721    };
722    let host = format!("{ip}:{port}");
723    let result = call(
724        &mut wire,
725        "GET",
726        &host,
727        &format!("{}/info", proto::API),
728        None,
729        0,
730        &mut |_| Ok(()),
731    );
732    wire.finish();
733    result.is_ok_and(|reply| reply.status == 200)
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use std::io::Read;
740
741    #[test]
742    fn a_generated_identity_round_trips_through_load() {
743        let made = Identity::generate("Test Mac").unwrap();
744        assert!(valid_pin(&made.fingerprint));
745        assert_eq!(made.fingerprint, sha256_hex(&made.cert_der));
746        let loaded = Identity::load(made.cert_der.clone(), made.key_der.clone()).unwrap();
747        assert_eq!(loaded.fingerprint, made.fingerprint);
748        assert_eq!(loaded.key_der, made.key_der);
749    }
750
751    #[test]
752    fn two_generated_identities_have_different_fingerprints() {
753        let a = Identity::generate("Same Name").unwrap();
754        let b = Identity::generate("Same Name").unwrap();
755        assert_ne!(a.fingerprint, b.fingerprint);
756    }
757
758    #[test]
759    fn corrupt_identity_bytes_are_refused() {
760        assert!(Identity::load(vec![1, 2, 3], vec![4, 5]).is_err());
761    }
762
763    #[test]
764    fn the_verifier_message_is_the_one_the_classifier_matches() {
765        let server = Identity::generate("Server").unwrap();
766        let other = Identity::generate("Other").unwrap();
767        let verifier = PinVerifier {
768            expected: Some(other.fingerprint.clone()),
769            algorithms: provider().signature_verification_algorithms,
770        };
771        let cert = CertificateDer::from(server.cert_der.clone());
772        let name = ServerName::IpAddress("127.0.0.1".parse::<IpAddr>().unwrap().into());
773        let Err(error) = verifier.verify_server_cert(&cert, &[], &name, &[], UnixTime::now())
774        else {
775            panic!("a different certificate is refused");
776        };
777        assert!(matches!(&error, rustls::Error::General(text) if text == MISMATCH_MESSAGE));
778    }
779
780    #[test]
781    fn a_pinned_handshake_with_another_certificate_is_an_identity_mismatch() {
782        let server = Identity::generate("Server").unwrap();
783        let other = Identity::generate("Other").unwrap();
784        let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap();
785        let port = listener.local_addr().unwrap().port();
786        let tls = server_config(&server).unwrap();
787        let accepting = std::thread::spawn(move || {
788            if let Ok((socket, _)) = listener.accept() {
789                let _ = socket.set_read_timeout(Some(Duration::from_secs(5)));
790                if let Ok(mut wire) = accept_tls(&tls, socket) {
791                    let mut buffer = [0u8; 16];
792                    let _ = wire.read(&mut buffer);
793                }
794            }
795        });
796        let ip: IpAddr = "127.0.0.1".parse().unwrap();
797        let mut wire = connect(ip, port, true, &other.fingerprint, Duration::from_secs(5)).unwrap();
798        let error = call(&mut wire, "GET", "127.0.0.1", "/", None, 0, &mut |_| Ok(()))
799            .err()
800            .expect("the handshake must fail");
801        assert!(is_identity_mismatch(&error));
802        drop(wire);
803        accepting.join().unwrap();
804    }
805}