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}