kona_sources/signer/remote/
client.rs

1use alloy_primitives::Address;
2use alloy_rpc_client::ClientBuilder;
3use alloy_transport_http::Http;
4use reqwest::header::HeaderMap;
5use std::sync::Arc;
6use thiserror::Error;
7use tokio::sync::RwLock;
8use url::Url;
9
10use crate::{
11    RemoteSignerHandler,
12    signer::remote::cert::{CertificateError, ClientCert},
13};
14
15/// Configuration for the remote signer client
16///
17/// This configuration supports various TLS/certificate scenarios:
18///
19/// 1. **Basic HTTPS**: Only `endpoint` and `address` are required.
20/// 2. **Custom CA**: Provide `ca_cert` to verify servers with custom/self-signed certificates.
21/// 3. **Mutual TLS (mTLS)**: Provide both `client_cert` and `client_key` for client authentication.
22/// 4. **Full mTLS with custom CA**: Combine all certificate options for maximum security.
23///
24/// Certificate formats supported:
25/// - PEM format for all certificates and keys
26/// - Certificates should be provided as file paths.
27///
28/// By default, the process will watch for changes in the client certificate files and reload the
29/// client automatically.
30#[derive(Debug, Clone)]
31pub struct RemoteSigner {
32    /// The URL of the remote signer endpoint
33    pub endpoint: Url,
34    /// The address of the signer.
35    pub address: Address,
36    /// Optional client certificate for mTLS (PEM format)
37    pub client_cert: Option<ClientCert>,
38    /// Optional CA certificate for server verification (PEM format)
39    pub ca_cert: Option<std::path::PathBuf>,
40    /// Headers to pass to the remote signer.
41    pub headers: HeaderMap,
42}
43
44/// Errors that can occur when starting a remote signer.
45#[derive(Debug, Error)]
46pub enum RemoteSignerStartError {
47    /// Failed to ping signer
48    #[error("Failed to ping signer: {0}")]
49    Ping(alloy_transport::TransportError),
50    /// HTTP client build error
51    #[error("HTTP client build error: {0}")]
52    HTTPClientBuild(#[from] reqwest::Error),
53    /// Invalid certificate error
54    #[error("Invalid certificate: {0}")]
55    Certificate(#[from] CertificateError),
56    /// Certificate watcher error
57    #[error("Certificate watcher error: {0}")]
58    CertificateWatcher(#[from] notify::Error),
59}
60
61impl RemoteSigner {
62    /// Creates a new remote signer with the given configuration
63    ///
64    /// If client certificates are configured, this will automatically start a certificate watcher
65    /// that monitors the certificate files for changes. When certificates are updated (e.g., by
66    /// cert-manager in Kubernetes), the TLS client will be automatically reloaded with the new
67    /// certificates without requiring a restart.
68    ///
69    /// # Certificate Watching
70    ///
71    /// The certificate watcher monitors:
72    /// - Client certificate file (if mTLS is configured)
73    /// - Client private key file (if mTLS is configured)
74    /// - CA certificate file (if custom CA is configured)
75    ///
76    /// When any of these files are modified, the watcher will:
77    /// 1. Log the certificate change event
78    /// 2. Reload the certificate files from disk
79    /// 3. Rebuild the HTTP client with the new TLS configuration
80    /// 4. Replace the existing client atomically
81    ///
82    /// This enables zero-downtime certificate rotation in production environments.
83    pub async fn start(self) -> Result<RemoteSignerHandler, RemoteSignerStartError> {
84        let http_client = self.build_http_client()?;
85        let transport = Http::with_client(http_client, self.endpoint.clone());
86        let client = ClientBuilder::default().transport(transport, true);
87
88        // Try to ping the signer to check if it's reachable
89        let version: String =
90            client.request("health_status", ()).await.map_err(RemoteSignerStartError::Ping)?;
91
92        tracing::info!(target: "signer", version, "Connected to op-signer server");
93
94        let client = Arc::new(RwLock::new(client));
95
96        // Start certificate watcher if client certificates are configured
97        let watcher_handle = self.start_certificate_watcher(client.clone()).await?;
98
99        Ok(RemoteSignerHandler { client, watcher_handle, address: self.address })
100    }
101
102    /// Builds an HTTP client with certificate handling for the remote signer
103    pub(super) fn build_http_client(&self) -> Result<reqwest::Client, RemoteSignerStartError> {
104        let mut client_builder = reqwest::Client::builder();
105
106        // Configure TLS if certificates are provided
107        if self.client_cert.is_some() || self.ca_cert.is_some() {
108            let tls_config = self.build_tls_config()?;
109            client_builder = client_builder.use_preconfigured_tls(tls_config);
110        }
111
112        // Set headers
113        client_builder = client_builder.default_headers(self.headers.clone());
114
115        client_builder.build().map_err(RemoteSignerStartError::HTTPClientBuild)
116    }
117}