1use crate::auth::x509_utils::sanitize_certificate_pem;
2use crate::core::region::Region;
3use base64::{Engine as _, engine::general_purpose};
4use chrono::{DateTime, Utc};
5use reqwest::Client;
6use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding};
7use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::time::Duration;
11use tokio::time::sleep;
12
13const MAX_ATTEMPTS: u32 = 3;
14const MAX_BACKOFF_SECS: u64 = 1;
15const REQUEST_TIMEOUT_SECS: u64 = 10;
16
17#[derive(Clone)]
19pub struct SessionKeyPair {
20 pub private_key_pem: String,
21 pub public_key_pem: String,
22 pub private_key: RsaPrivateKey,
23}
24
25#[derive(Debug, Clone)]
27pub struct SecurityToken {
28 pub token: String,
29 pub expires_at: DateTime<Utc>,
30}
31
32#[derive(Serialize)]
34struct FederationRequest {
35 certificate: String,
36 #[serde(rename = "publicKey")]
37 public_key: String,
38 #[serde(rename = "intermediateCertificates")]
39 intermediate_certificates: Vec<String>,
40 purpose: String,
41}
42
43#[derive(Deserialize)]
45struct FederationResponse {
46 token: String,
47}
48
49pub fn generate_session_keypair() -> crate::core::Result<SessionKeyPair> {
51 use rand::rngs::OsRng;
52
53 let mut rng = OsRng;
54 let private_key = RsaPrivateKey::new(&mut rng, 2048).map_err(|e| {
55 crate::core::OciError::AuthError(format!("Failed to generate session keypair: {}", e))
56 })?;
57
58 let public_key = RsaPublicKey::from(&private_key);
59
60 let private_key_pem = private_key
61 .to_pkcs8_pem(LineEnding::LF)
62 .map_err(|e| {
63 crate::core::OciError::AuthError(format!("Failed to encode private key: {}", e))
64 })?
65 .to_string();
66
67 let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).map_err(|e| {
68 crate::core::OciError::AuthError(format!("Failed to encode public key: {}", e))
69 })?;
70
71 Ok(SessionKeyPair {
72 private_key_pem,
73 public_key_pem,
74 private_key,
75 })
76}
77
78pub async fn request_security_token(
80 region: &Region,
81 tenancy_id: &str,
82 leaf_cert_pem: &str,
83 leaf_key_pem: &str,
84 intermediate_certs_pem: &[String],
85 session_public_key_pem: &str,
86) -> crate::core::Result<SecurityToken> {
87 let endpoint = format!("https://auth.{}.oraclecloud.com/v1/x509", region.id());
88
89 let leaf_private_key = RsaPrivateKey::from_pkcs8_pem(leaf_key_pem).map_err(|e| {
91 crate::core::OciError::AuthError(format!("Failed to parse leaf private key: {}", e))
92 })?;
93
94 let request_body = FederationRequest {
96 certificate: sanitize_certificate_pem(leaf_cert_pem),
97 public_key: sanitize_certificate_pem(session_public_key_pem),
98 intermediate_certificates: intermediate_certs_pem
99 .iter()
100 .map(|cert| sanitize_certificate_pem(cert))
101 .collect(),
102 purpose: "DEFAULT".to_string(),
103 };
104
105 let body_json = serde_json::to_string(&request_body).map_err(|e| {
106 crate::core::OciError::AuthError(format!("Failed to serialize request: {}", e))
107 })?;
108
109 let client = Client::builder()
111 .timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
112 .build()
113 .map_err(|e| {
114 crate::core::OciError::AuthError(format!("Failed to create HTTP client: {}", e))
115 })?;
116
117 let mut last_error = None;
118
119 for attempt in 1..=MAX_ATTEMPTS {
120 match make_federation_request(
121 &client,
122 &endpoint,
123 &body_json,
124 tenancy_id,
125 &leaf_private_key,
126 )
127 .await
128 {
129 Ok(token) => return parse_security_token(token),
130 Err(e) => {
131 if !is_retryable_error(&e) {
133 return Err(e);
134 }
135
136 last_error = Some(e);
137
138 if attempt < MAX_ATTEMPTS {
140 let backoff = calculate_backoff_with_jitter(attempt, MAX_BACKOFF_SECS);
141 sleep(backoff).await;
142 }
143 }
144 }
145 }
146
147 Err(last_error.unwrap_or_else(|| {
148 crate::core::OciError::AuthError("Federation request failed with no error".to_string())
149 }))
150}
151
152async fn make_federation_request(
154 client: &Client,
155 endpoint: &str,
156 body_json: &str,
157 tenancy_id: &str,
158 leaf_private_key: &RsaPrivateKey,
159) -> crate::core::Result<String> {
160 let url = url::Url::parse(endpoint).map_err(|e| {
161 crate::core::OciError::AuthError(format!("Invalid federation endpoint: {}", e))
162 })?;
163
164 let mut headers = reqwest::header::HeaderMap::new();
165
166 let now = chrono::Utc::now();
168 headers.insert(
169 "x-date",
170 now.format("%a, %d %b %Y %H:%M:%S GMT")
171 .to_string()
172 .parse()
173 .map_err(|e| crate::core::OciError::AuthError(format!("Invalid date header: {}", e)))?,
174 );
175
176 headers.insert(
177 "host",
178 url.host_str()
179 .ok_or_else(|| crate::core::OciError::AuthError("Missing host in URL".to_string()))?
180 .parse()
181 .map_err(|e| crate::core::OciError::AuthError(format!("Invalid host header: {}", e)))?,
182 );
183
184 headers.insert(
185 "content-type",
186 "application/json".parse().map_err(|e| {
187 crate::core::OciError::AuthError(format!("Invalid content-type: {}", e))
188 })?,
189 );
190
191 headers.insert(
192 "content-length",
193 body_json.len().to_string().parse().map_err(|e| {
194 crate::core::OciError::AuthError(format!("Invalid content-length: {}", e))
195 })?,
196 );
197
198 let mut hasher = Sha256::new();
200 hasher.update(body_json.as_bytes());
201 let hash = hasher.finalize();
202 let b64_hash = general_purpose::STANDARD.encode(hash);
203
204 headers.insert(
205 "x-content-sha256",
206 b64_hash.parse().map_err(|e| {
207 crate::core::OciError::AuthError(format!("Invalid sha256 header: {}", e))
208 })?,
209 );
210
211 sign_federation_request(&url, &mut headers, body_json, tenancy_id, leaf_private_key)?;
213
214 let response = client
216 .post(endpoint)
217 .headers(headers)
218 .body(body_json.to_string())
219 .send()
220 .await
221 .map_err(|e| {
222 crate::core::OciError::AuthError(format!("Federation request failed: {}", e))
223 })?;
224
225 let status = response.status();
226
227 if status.is_success() {
228 let federation_response: FederationResponse = response.json().await.map_err(|e| {
229 crate::core::OciError::AuthError(format!("Failed to parse federation response: {}", e))
230 })?;
231 Ok(federation_response.token)
232 } else if status.is_client_error() {
233 let error_body = response.text().await.unwrap_or_default();
235 Err(crate::core::OciError::ConfigError(format!(
236 "Federation request failed with status {}: {}",
237 status, error_body
238 )))
239 } else {
240 let error_body = response.text().await.unwrap_or_default();
242 Err(crate::core::OciError::ServiceError {
243 status: status.as_u16(),
244 code: "FederationServiceError".to_string(),
245 message: error_body,
246 })
247 }
248}
249
250fn sign_federation_request(
252 url: &url::Url,
253 headers: &mut reqwest::header::HeaderMap,
254 _body: &str,
255 tenancy_id: &str,
256 leaf_private_key: &RsaPrivateKey,
257) -> crate::core::Result<()> {
258 let headers_to_sign = vec![
260 "(request-target)",
261 "host",
262 "x-date",
263 "content-type",
264 "content-length",
265 "x-content-sha256",
266 ];
267
268 let mut parts = Vec::new();
270 for header_name in &headers_to_sign {
271 let value = if *header_name == "(request-target)" {
272 format!("post {}", url.path())
273 } else {
274 headers
275 .get(*header_name)
276 .and_then(|v| v.to_str().ok())
277 .ok_or_else(|| {
278 crate::core::OciError::SigningError(format!("Missing header: {}", header_name))
279 })?
280 .to_string()
281 };
282 parts.push(format!("{}: {}", header_name, value));
283 }
284 let signing_string = parts.join("\n");
285
286 let mut hasher = Sha256::new();
288 hasher.update(signing_string.as_bytes());
289 let hashed = hasher.finalize();
290
291 let padding = Pkcs1v15Sign::new_unprefixed();
292 let signature = leaf_private_key
293 .sign(padding, &hashed)
294 .map_err(|e| crate::core::OciError::SigningError(format!("Failed to sign: {}", e)))?;
295
296 let signature_b64 = general_purpose::STANDARD.encode(&signature);
297
298 let key_id = format!("{}/fed-x509", tenancy_id);
302
303 let auth_header = format!(
304 r#"Signature version="1",headers="{}",keyId="{}",algorithm="rsa-sha256",signature="{}""#,
305 headers_to_sign.join(" "),
306 key_id,
307 signature_b64
308 );
309
310 headers.insert(
311 "authorization",
312 auth_header.parse().map_err(|e| {
313 crate::core::OciError::SigningError(format!("Invalid authorization header: {}", e))
314 })?,
315 );
316
317 Ok(())
318}
319
320fn parse_security_token(token: String) -> crate::core::Result<SecurityToken> {
322 use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
323
324 #[derive(Deserialize)]
325 struct Claims {
326 exp: i64,
327 }
328
329 let mut validation = Validation::new(Algorithm::RS256);
331 validation.insecure_disable_signature_validation();
332 validation.validate_exp = false;
333
334 let token_data = decode::<Claims>(&token, &DecodingKey::from_secret(&[]), &validation)
335 .map_err(|e| crate::core::OciError::AuthError(format!("Failed to parse JWT: {}", e)))?;
336
337 let expires_at = DateTime::from_timestamp(token_data.claims.exp, 0).ok_or_else(|| {
338 crate::core::OciError::AuthError("Invalid expiration timestamp in JWT".to_string())
339 })?;
340
341 Ok(SecurityToken { token, expires_at })
342}
343
344fn is_retryable_error(error: &crate::core::OciError) -> bool {
346 match error {
347 crate::core::OciError::ServiceError { status, .. } => *status >= 500,
348 crate::core::OciError::HttpError(_) => true, crate::core::OciError::AuthError(_) => true, _ => false,
351 }
352}
353
354fn calculate_backoff_with_jitter(attempt: u32, max_secs: u64) -> Duration {
356 use rand::Rng;
357
358 let base_ms = 100u64 * 2u64.pow(attempt - 1);
359 let max_ms = max_secs * 1000;
360 let backoff_ms = base_ms.min(max_ms);
361
362 let mut rng = rand::thread_rng();
363 let jitter_ms = rng.gen_range(0..=backoff_ms);
364
365 Duration::from_millis(jitter_ms)
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_generate_session_keypair() {
374 let keypair = generate_session_keypair().unwrap();
375 assert!(keypair.private_key_pem.contains("BEGIN PRIVATE KEY"));
376 assert!(keypair.public_key_pem.contains("BEGIN PUBLIC KEY"));
377 }
378
379 #[test]
380 fn test_sanitize_certificate() {
381 let cert = r#"-----BEGIN CERTIFICATE-----
382MIIC...
383-----END CERTIFICATE-----"#;
384 let sanitized = sanitize_certificate_pem(cert);
385 assert!(!sanitized.contains("BEGIN"));
386 assert!(sanitized.starts_with("MIIC"));
387 }
388
389 #[test]
390 fn test_parse_jwt_token() {
391 let token =
393 "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDQwNjcyMDAsInN1YiI6InRlc3QifQ.test";
394
395 let result = parse_security_token(token.to_string()).unwrap();
396 assert_eq!(result.token, token);
397 assert_eq!(result.expires_at.to_rfc3339(), "2024-01-01T00:00:00+00:00");
399 }
400
401 #[test]
402 fn test_is_retryable_error() {
403 let error = crate::core::OciError::ServiceError {
405 status: 500,
406 code: "InternalError".to_string(),
407 message: "Internal server error".to_string(),
408 };
409 assert!(is_retryable_error(&error));
410
411 let error = crate::core::OciError::ConfigError("Bad request".to_string());
413 assert!(!is_retryable_error(&error));
414 }
415}