posemesh_compute_node/auth/
siwe_after_registration.rs

1use super::siwe;
2use super::token_manager::{
3    AccessAuthenticator, SystemClock, TokenManager, TokenManagerConfig, TokenProvider,
4    TokenProviderError,
5};
6use crate::config::NodeConfig;
7use anyhow::{anyhow, Result};
8use async_trait::async_trait;
9use posemesh_node_registration::persist as dds_state;
10use sha3::{Digest, Keccak256};
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::Mutex;
14use tokio::time::sleep;
15use tracing::{info, warn};
16
17type ManagerCell = Arc<Mutex<Option<Arc<SiweTokenManager>>>>;
18type SiweTokenManager = TokenManager<DdsAuthenticator, SystemClock>;
19
20#[derive(Clone)]
21struct DdsAuthenticator {
22    base_url: Arc<String>,
23    priv_hex: Arc<String>,
24    address: Arc<String>,
25}
26
27impl DdsAuthenticator {
28    fn new(base_url: String, priv_hex: String) -> Result<Self> {
29        let address = derive_eth_address(&priv_hex)?;
30        Ok(Self {
31            base_url: Arc::new(base_url),
32            priv_hex: Arc::new(priv_hex),
33            address: Arc::new(address),
34        })
35    }
36}
37
38#[async_trait]
39impl AccessAuthenticator for DdsAuthenticator {
40    async fn login(&self) -> Result<super::siwe::AccessBundle, super::siwe::SiweError> {
41        let meta = siwe::request_nonce(self.base_url.as_str(), self.address.as_str()).await?;
42        let message = siwe::compose_message(&meta, self.address.as_str(), None)?;
43        let signature = siwe::sign_message(self.priv_hex.as_str(), &message)?;
44        siwe::verify(
45            self.base_url.as_str(),
46            self.address.as_str(),
47            &message,
48            &signature,
49        )
50        .await
51    }
52}
53
54pub struct SiweAfterRegistration {
55    authenticator: Arc<DdsAuthenticator>,
56    config: TokenManagerConfig,
57    manager: ManagerCell,
58}
59
60impl SiweAfterRegistration {
61    pub fn from_config(cfg: &NodeConfig) -> Result<Self> {
62        let dds_base_url = cfg
63            .dds_base_url
64            .as_ref()
65            .ok_or_else(|| anyhow!("DDS_BASE_URL required for DDS SIWE authentication"))?
66            .as_str()
67            .to_string();
68
69        let priv_hex = cfg
70            .secp256k1_privhex
71            .as_ref()
72            .filter(|value| !value.trim().is_empty())
73            .ok_or_else(|| anyhow!("SECP256K1_PRIVHEX required for DDS SIWE authentication"))?
74            .to_string();
75
76        let config = TokenManagerConfig {
77            safety_ratio: cfg.token_safety_ratio as f64,
78            max_retries: cfg.token_reauth_max_retries,
79            jitter: Duration::from_millis(cfg.token_reauth_jitter_ms),
80        };
81
82        Self::new(dds_base_url, priv_hex, config)
83    }
84
85    pub fn new(dds_base_url: String, priv_hex: String, config: TokenManagerConfig) -> Result<Self> {
86        let authenticator = Arc::new(DdsAuthenticator::new(dds_base_url, priv_hex)?);
87        Ok(Self {
88            authenticator,
89            config,
90            manager: Arc::new(Mutex::new(None)),
91        })
92    }
93
94    pub async fn start(&self) -> Result<SiweHandle> {
95        let manager = self.ensure_started().await?;
96        Ok(SiweHandle { manager })
97    }
98
99    async fn ensure_started(&self) -> Result<Arc<SiweTokenManager>> {
100        {
101            let guard = self.manager.lock().await;
102            if let Some(existing) = guard.as_ref() {
103                return Ok(existing.clone());
104            }
105        }
106
107        self.wait_for_registration().await?;
108
109        let manager: Arc<SiweTokenManager> = Arc::new(TokenManager::new(
110            Arc::clone(&self.authenticator),
111            Arc::new(SystemClock),
112            self.config.clone(),
113        ));
114        manager.start_bg().await;
115
116        manager
117            .bearer()
118            .await
119            .map_err(|err| anyhow!("initial DDS SIWE login failed: {err}"))?;
120
121        let mut guard = self.manager.lock().await;
122        if let Some(existing) = guard.as_ref() {
123            manager.stop_bg().await;
124            return Ok(existing.clone());
125        }
126        *guard = Some(manager.clone());
127        Ok(manager)
128    }
129
130    async fn wait_for_registration(&self) -> Result<()> {
131        loop {
132            match dds_state::read_node_secret() {
133                Ok(Some(_)) => {
134                    info!("DDS registration confirmed; starting SIWE token manager");
135                    return Ok(());
136                }
137                Ok(None) => {
138                    sleep(Duration::from_secs(1)).await;
139                }
140                Err(err) => {
141                    warn!(error = %err, "Failed to read DDS registration status; retrying");
142                    sleep(Duration::from_secs(1)).await;
143                }
144            }
145        }
146    }
147}
148
149#[derive(Clone)]
150pub struct SiweHandle {
151    manager: Arc<SiweTokenManager>,
152}
153
154impl SiweHandle {
155    pub async fn bearer(&self) -> Result<String, TokenProviderError> {
156        self.manager.bearer().await
157    }
158
159    pub async fn shutdown(&self) {
160        self.manager.stop_bg().await;
161    }
162}
163
164#[async_trait]
165impl TokenProvider for SiweHandle {
166    async fn bearer(&self) -> crate::auth::token_manager::TokenProviderResult<String> {
167        // Delegate to internal manager
168        self.manager.bearer().await
169    }
170
171    async fn on_unauthorized(&self) {
172        // Force early refresh on next bearer() call
173        self.manager.on_unauthorized_retry().await;
174    }
175}
176
177fn derive_eth_address(priv_hex: &str) -> Result<String> {
178    use k256::{ecdsa::SigningKey, FieldBytes};
179
180    let trimmed = priv_hex.trim_start_matches("0x");
181    let key_bytes =
182        hex::decode(trimmed).map_err(|_| anyhow!("invalid secp256k1 private key hex"))?;
183    if key_bytes.len() != 32 {
184        return Err(anyhow!("secp256k1 private key must be 32 bytes"));
185    }
186    let mut key = [0u8; 32];
187    key.copy_from_slice(&key_bytes);
188    let field_bytes: FieldBytes = key.into();
189    let signing_key = SigningKey::from_bytes(&field_bytes)
190        .map_err(|e| anyhow!("failed to construct signing key: {e}"))?;
191    let verifying_key = signing_key.verifying_key();
192    let encoded = verifying_key.to_encoded_point(false);
193    let pubkey = encoded.as_bytes();
194
195    let mut hasher = Keccak256::new();
196    hasher.update(&pubkey[1..]);
197    let hashed = hasher.finalize();
198    let address_bytes = &hashed[12..];
199    Ok(format!("0x{}", hex::encode(address_bytes)))
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use url::Url;
206
207    fn base_cfg() -> NodeConfig {
208        NodeConfig {
209            dms_base_url: Url::parse("https://dms.example").unwrap(),
210            node_version: "1.0.0".into(),
211            request_timeout_secs: 10,
212            dds_base_url: None,
213            node_url: None,
214            reg_secret: None,
215            secp256k1_privhex: None,
216            heartbeat_jitter_ms: 250,
217            poll_backoff_ms_min: 1000,
218            poll_backoff_ms_max: 30000,
219            token_safety_ratio: 0.75,
220            token_reauth_max_retries: 3,
221            token_reauth_jitter_ms: 500,
222            register_interval_secs: None,
223            register_max_retry: None,
224            max_concurrency: 1,
225            log_format: crate::config::LogFormat::Json,
226            enable_noop: true,
227            noop_sleep_secs: 1,
228        }
229    }
230
231    #[test]
232    fn from_config_errors_when_missing_siwe_fields() {
233        let cfg = base_cfg();
234        assert!(SiweAfterRegistration::from_config(&cfg).is_err());
235    }
236
237    #[test]
238    fn derive_eth_address_matches_expected_value() {
239        let priv_hex = "4c0883a69102937d6231471b5dbb6204fe5129617082798ce3f4fdf2548b6f90";
240        let addr = derive_eth_address(priv_hex).expect("address");
241        assert_eq!(addr, "0xfdbb6caf01414300c16ea14859fec7736d95355f");
242    }
243
244    #[test]
245    fn from_config_produces_instance_when_siwe_configured() {
246        let mut cfg = base_cfg();
247        cfg.dds_base_url = Some(Url::parse("https://dds.example").unwrap());
248        cfg.secp256k1_privhex =
249            Some("4c0883a69102937d6231471b5dbb6204fe5129617082798ce3f4fdf2548b6f90".into());
250
251        assert!(SiweAfterRegistration::from_config(&cfg).is_ok());
252    }
253}