1use crate::errors::{AuthError, Result};
7use crate::saml_assertions::SamlAssertion;
8use base64::{Engine as _, engine::general_purpose::STANDARD};
9use chrono::{DateTime, Duration, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Default)]
15pub struct WsSecurityHeader {
16 pub username_token: Option<UsernameToken>,
18
19 pub timestamp: Option<Timestamp>,
21
22 pub binary_security_token: Option<BinarySecurityToken>,
24
25 pub saml_assertions: Vec<SamlAssertionRef>,
27
28 pub signature: Option<WsSecuritySignature>,
30
31 pub custom_elements: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct UsernameToken {
38 pub username: String,
40
41 pub password: Option<UsernamePassword>,
43
44 pub nonce: Option<String>,
46
47 pub created: Option<DateTime<Utc>>,
49
50 pub wsu_id: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct UsernamePassword {
57 pub value: String,
59
60 pub password_type: PasswordType,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub enum PasswordType {
67 PasswordText,
69
70 PasswordDigest,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Timestamp {
77 pub created: DateTime<Utc>,
79
80 pub expires: DateTime<Utc>,
82
83 pub wsu_id: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct BinarySecurityToken {
90 pub value: String,
92
93 pub value_type: String,
95
96 pub encoding_type: String,
98
99 pub wsu_id: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct SamlAssertionRef {
106 pub assertion: SamlAssertion,
108
109 pub wsu_id: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct WsSecuritySignature {
116 pub signature_method: String,
118
119 pub canonicalization_method: String,
121
122 pub digest_method: String,
124
125 pub references: Vec<SignatureReference>,
127
128 pub key_info: Option<KeyInfo>,
130
131 pub signature_value: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct SignatureReference {
138 pub uri: String,
140
141 pub digest_value: String,
143
144 pub transforms: Vec<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct KeyInfo {
151 pub security_token_reference: Option<String>,
153
154 pub key_value: Option<String>,
156
157 pub x509_data: Option<String>,
159}
160
161#[derive(Debug, Clone)]
163pub struct WsSecurityConfig {
164 pub include_timestamp: bool,
166
167 pub timestamp_ttl: Duration,
169
170 pub sign_message: bool,
172
173 pub elements_to_sign: Vec<String>,
175
176 pub signing_certificate: Option<Vec<u8>>,
178
179 pub signing_private_key: Option<Vec<u8>>,
181
182 pub include_certificate: bool,
184
185 pub saml_token_endpoint: Option<String>,
187
188 pub actor: Option<String>,
190}
191
192pub struct WsSecurityClient {
194 config: WsSecurityConfig,
196
197 namespaces: HashMap<String, String>,
199}
200
201impl WsSecurityClient {
202 pub fn new(config: WsSecurityConfig) -> Self {
204 let mut namespaces = HashMap::new();
205 namespaces.insert(
206 "wsse".to_string(),
207 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
208 .to_string(),
209 );
210 namespaces.insert(
211 "wsu".to_string(),
212 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
213 .to_string(),
214 );
215 namespaces.insert(
216 "ds".to_string(),
217 "http://www.w3.org/2000/09/xmldsig#".to_string(),
218 );
219 namespaces.insert(
220 "saml".to_string(),
221 "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
222 );
223
224 Self { config, namespaces }
225 }
226
227 pub fn create_username_token_header(
229 &self,
230 username: &str,
231 password: Option<&str>,
232 password_type: PasswordType,
233 ) -> Result<WsSecurityHeader> {
234 let mut header = WsSecurityHeader::default();
235
236 let (nonce, created) = if password_type == PasswordType::PasswordDigest {
237 (Some(self.generate_nonce()), Some(Utc::now()))
238 } else {
239 (None, None)
240 };
241
242 let password_element = if let Some(pwd) = password {
243 let pwd_value = match password_type {
244 PasswordType::PasswordText => pwd.to_string(),
245 PasswordType::PasswordDigest => {
246 self.compute_password_digest(pwd, nonce.as_ref().unwrap(), &created.unwrap())?
247 }
248 };
249
250 Some(UsernamePassword {
251 value: pwd_value,
252 password_type,
253 })
254 } else {
255 None
256 };
257
258 header.username_token = Some(UsernameToken {
259 username: username.to_string(),
260 password: password_element,
261 nonce,
262 created,
263 wsu_id: Some(format!("UsernameToken-{}", uuid::Uuid::new_v4())),
264 });
265
266 if self.config.include_timestamp {
267 header.timestamp = Some(self.create_timestamp());
268 }
269
270 Ok(header)
271 }
272
273 pub fn create_certificate_header(&self, certificate: &[u8]) -> Result<WsSecurityHeader> {
275 let mut header = WsSecurityHeader::default();
276
277 let cert_b64 = STANDARD.encode(certificate);
279
280 header.binary_security_token = Some(BinarySecurityToken {
281 value: cert_b64,
282 value_type: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3".to_string(),
283 encoding_type: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary".to_string(),
284 wsu_id: Some(format!("X509Token-{}", uuid::Uuid::new_v4())),
285 });
286
287 if self.config.include_timestamp {
288 header.timestamp = Some(self.create_timestamp());
289 }
290
291 if self.config.sign_message {
292 header.signature = Some(self.create_signature_template()?);
293 }
294
295 Ok(header)
296 }
297
298 pub fn create_saml_header(&self, assertion: SamlAssertion) -> Result<WsSecurityHeader> {
300 let mut header = WsSecurityHeader::default();
301
302 let assertion_ref = SamlAssertionRef {
303 assertion,
304 wsu_id: Some(format!("SamlAssertion-{}", uuid::Uuid::new_v4())),
305 };
306
307 header.saml_assertions.push(assertion_ref);
308
309 if self.config.include_timestamp {
310 header.timestamp = Some(self.create_timestamp());
311 }
312
313 Ok(header)
314 }
315 pub fn header_to_xml(&self, header: &WsSecurityHeader) -> Result<String> {
317 let mut xml = String::new();
318
319 xml.push_str(&format!(
321 r#"<wsse:Security xmlns:wsse="{}" xmlns:wsu="{}">"#,
322 self.namespaces["wsse"], self.namespaces["wsu"]
323 ));
324
325 if let Some(ref timestamp) = header.timestamp {
327 xml.push_str(&self.timestamp_to_xml(timestamp));
328 }
329
330 if let Some(ref username_token) = header.username_token {
332 xml.push_str(&self.username_token_to_xml(username_token));
333 }
334
335 if let Some(ref bst) = header.binary_security_token {
337 xml.push_str(&self.binary_security_token_to_xml(bst));
338 }
339
340 for assertion_ref in &header.saml_assertions {
342 let assertion_xml = assertion_ref.assertion.to_xml()?;
343 xml.push_str(&assertion_xml);
344 }
345
346 if let Some(ref signature) = header.signature {
348 xml.push_str(&self.signature_to_xml(signature));
349 }
350
351 xml.push_str("</wsse:Security>");
353
354 Ok(xml)
355 }
356
357 fn generate_nonce(&self) -> String {
359 use rand::RngCore;
360 let mut rng = rand::rng();
361 let mut nonce = [0u8; 16];
362 rng.fill_bytes(&mut nonce);
363 STANDARD.encode(nonce)
364 }
365
366 fn compute_password_digest(
368 &self,
369 password: &str,
370 nonce: &str,
371 created: &DateTime<Utc>,
372 ) -> Result<String> {
373 use sha1::{Digest, Sha1};
374
375 let nonce_bytes = STANDARD
376 .decode(nonce)
377 .map_err(|_| AuthError::auth_method("ws_security", "Invalid nonce encoding"))?;
378 let created_str = created.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
379
380 let mut hasher = Sha1::new();
381 hasher.update(&nonce_bytes);
382 hasher.update(created_str.as_bytes());
383 hasher.update(password.as_bytes());
384
385 let digest = hasher.finalize();
386 Ok(STANDARD.encode(digest))
387 }
388
389 fn create_timestamp(&self) -> Timestamp {
391 let now = Utc::now();
392 let expires = now + self.config.timestamp_ttl;
393
394 Timestamp {
395 created: now,
396 expires,
397 wsu_id: Some(format!("Timestamp-{}", uuid::Uuid::new_v4())),
398 }
399 }
400
401 fn create_signature_template(&self) -> Result<WsSecuritySignature> {
403 Ok(WsSecuritySignature {
404 signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256".to_string(),
405 canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#".to_string(),
406 digest_method: "http://www.w3.org/2001/04/xmlenc#sha256".to_string(),
407 references: self
408 .config
409 .elements_to_sign
410 .iter()
411 .map(|element| {
412 SignatureReference {
413 uri: format!("#{}", element),
414 digest_value: String::new(), transforms: vec!["http://www.w3.org/2001/10/xml-exc-c14n#".to_string()],
416 }
417 })
418 .collect(),
419 key_info: None, signature_value: None, })
422 }
423
424 fn timestamp_to_xml(&self, timestamp: &Timestamp) -> String {
426 let mut xml = String::new();
427
428 if let Some(ref id) = timestamp.wsu_id {
429 xml.push_str(&format!(r#"<wsu:Timestamp wsu:Id="{}">"#, id));
430 } else {
431 xml.push_str("<wsu:Timestamp>");
432 }
433
434 xml.push_str(&format!(
435 "<wsu:Created>{}</wsu:Created>",
436 timestamp.created.format("%Y-%m-%dT%H:%M:%S%.3fZ")
437 ));
438
439 xml.push_str(&format!(
440 "<wsu:Expires>{}</wsu:Expires>",
441 timestamp.expires.format("%Y-%m-%dT%H:%M:%S%.3fZ")
442 ));
443
444 xml.push_str("</wsu:Timestamp>");
445 xml
446 }
447
448 fn username_token_to_xml(&self, token: &UsernameToken) -> String {
450 let mut xml = String::new();
451
452 if let Some(ref id) = token.wsu_id {
453 xml.push_str(&format!(r#"<wsse:UsernameToken wsu:Id="{}">"#, id));
454 } else {
455 xml.push_str("<wsse:UsernameToken>");
456 }
457
458 xml.push_str(&format!(
459 "<wsse:Username>{}</wsse:Username>",
460 token.username
461 ));
462
463 if let Some(ref password) = token.password {
464 let type_attr = match password.password_type {
465 PasswordType::PasswordText => {
466 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
467 }
468 PasswordType::PasswordDigest => {
469 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
470 }
471 };
472
473 xml.push_str(&format!(
474 r#"<wsse:Password Type="{}">{}</wsse:Password>"#,
475 type_attr, password.value
476 ));
477 }
478
479 if let Some(ref nonce) = token.nonce {
480 xml.push_str(&format!(
481 r#"<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">{}</wsse:Nonce>"#,
482 nonce
483 ));
484 }
485
486 if let Some(ref created) = token.created {
487 xml.push_str(&format!(
488 "<wsu:Created>{}</wsu:Created>",
489 created.format("%Y-%m-%dT%H:%M:%S%.3fZ")
490 ));
491 }
492
493 xml.push_str("</wsse:UsernameToken>");
494 xml
495 }
496
497 fn binary_security_token_to_xml(&self, token: &BinarySecurityToken) -> String {
499 let mut xml = String::new();
500
501 xml.push_str(&format!(
502 r#"<wsse:BinarySecurityToken ValueType="{}" EncodingType="{}""#,
503 token.value_type, token.encoding_type
504 ));
505
506 if let Some(ref id) = token.wsu_id {
507 xml.push_str(&format!(r#" wsu:Id="{}""#, id));
508 }
509
510 xml.push('>');
511 xml.push_str(&token.value);
512 xml.push_str("</wsse:BinarySecurityToken>");
513
514 xml
515 }
516
517 fn signature_to_xml(&self, signature: &WsSecuritySignature) -> String {
519 format!(
520 r#"<ds:Signature xmlns:ds="{}">
521 <ds:SignedInfo>
522 <ds:CanonicalizationMethod Algorithm="{}"/>
523 <ds:SignatureMethod Algorithm="{}"/>
524 {}
525 </ds:SignedInfo>
526 <ds:SignatureValue></ds:SignatureValue>
527 <ds:KeyInfo></ds:KeyInfo>
528 </ds:Signature>"#,
529 self.namespaces["ds"],
530 signature.canonicalization_method,
531 signature.signature_method,
532 signature
533 .references
534 .iter()
535 .map(|r| format!(
536 r#"<ds:Reference URI="{}">
537 <ds:Transforms>
538 {}
539 </ds:Transforms>
540 <ds:DigestMethod Algorithm="{}"/>
541 <ds:DigestValue></ds:DigestValue>
542 </ds:Reference>"#,
543 r.uri,
544 r.transforms
545 .iter()
546 .map(|t| format!(r#"<ds:Transform Algorithm="{}"/>"#, t))
547 .collect::<Vec<_>>()
548 .join(""),
549 signature.digest_method
550 ))
551 .collect::<Vec<_>>()
552 .join("")
553 )
554 }
555}
556
557impl Default for WsSecurityConfig {
558 fn default() -> Self {
559 Self {
560 include_timestamp: true,
561 timestamp_ttl: Duration::minutes(5),
562 sign_message: false,
563 elements_to_sign: vec!["Body".to_string(), "Timestamp".to_string()],
564 signing_certificate: None,
565 signing_private_key: None,
566 include_certificate: true,
567 saml_token_endpoint: None,
568 actor: None,
569 }
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_username_token_creation() {
579 let config = WsSecurityConfig::default();
580 let client = WsSecurityClient::new(config);
581
582 let header = client
583 .create_username_token_header("testuser", Some("testpass"), PasswordType::PasswordText)
584 .unwrap();
585
586 assert!(header.username_token.is_some());
587 let token = header.username_token.unwrap();
588 assert_eq!(token.username, "testuser");
589 assert!(token.password.is_some());
590 }
591
592 #[test]
593 fn test_password_digest() {
594 let config = WsSecurityConfig::default();
595 let client = WsSecurityClient::new(config);
596
597 let nonce = "MTIzNDU2Nzg5MDEyMzQ1Ng=="; let created = DateTime::parse_from_rfc3339("2023-01-01T12:00:00Z")
599 .unwrap()
600 .with_timezone(&Utc);
601 let password = "secret";
602
603 let digest = client
604 .compute_password_digest(password, nonce, &created)
605 .unwrap();
606 assert!(!digest.is_empty());
607 }
608
609 #[test]
610 fn test_timestamp_creation() {
611 let config = WsSecurityConfig::default();
612 let client = WsSecurityClient::new(config);
613
614 let timestamp = client.create_timestamp();
615 assert!(timestamp.expires > timestamp.created);
616 assert!(timestamp.wsu_id.is_some());
617 }
618
619 #[test]
620 fn test_xml_generation() {
621 let config = WsSecurityConfig::default();
622 let client = WsSecurityClient::new(config);
623
624 let header = client
625 .create_username_token_header("testuser", Some("testpass"), PasswordType::PasswordText)
626 .unwrap();
627
628 let xml = client.header_to_xml(&header).unwrap();
629 assert!(xml.contains("<wsse:Security"));
630 assert!(xml.contains("<wsse:UsernameToken"));
631 assert!(xml.contains("testuser"));
632 assert!(xml.contains("</wsse:Security>"));
633 }
634
635 #[test]
636 fn test_certificate_header() {
637 let config = WsSecurityConfig::default();
638 let client = WsSecurityClient::new(config);
639
640 let dummy_cert = b"dummy certificate data";
641 let header = client.create_certificate_header(dummy_cert).unwrap();
642
643 assert!(header.binary_security_token.is_some());
644 let bst = header.binary_security_token.unwrap();
645 assert_eq!(bst.value, STANDARD.encode(dummy_cert));
646 }
647}