1#![allow(clippy::doc_overindented_list_items)]
2use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21use crate::bridge_spiffe::{parse_spiffe_id, spiffe_to_actor_id, ParsedSpiffeId};
22use crate::bridges::{Bridge, BridgeError, BridgeKind};
23use crate::generated::{
24 ActorIdentity, ActorIdentity_IdentityVersion, ActorType, AuthorityRoot, AuthorityRoot_Kind,
25 PublicKey, PublicKey_Purpose, TrustLevel,
26};
27
28use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
29use base64::Engine as _;
30
31#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39pub struct XfccEntry {
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub uri: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub hash: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub by: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub subject: Option<String>,
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
61pub struct IstioPrincipal {
62 pub spiffe_id: String,
64 pub namespace: String,
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
71pub struct LinkerdClient {
72 pub spiffe_id: String,
74}
75
76#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
80pub struct ProofEventStub {
81 pub event_type: String,
83 pub payload: Value,
85}
86
87#[derive(Clone, Debug, Default)]
88pub struct ServiceMeshBridgeConfig {
89 pub bridge_id: String,
90 pub trust_domain: String,
91}
92
93pub struct ServiceMeshBridge {
94 cfg: ServiceMeshBridgeConfig,
95}
96
97pub fn parse_xfcc(header: &str) -> Result<Vec<XfccEntry>, BridgeError> {
111 let header = header.trim();
112 if header.is_empty() {
113 return Err(BridgeError::InvalidInput("empty XFCC header".into()));
114 }
115 let raw_entries = split_xfcc_entries(header)?;
116 let mut out = Vec::with_capacity(raw_entries.len());
117 for raw in raw_entries {
118 out.push(parse_xfcc_entry(&raw)?);
119 }
120 if out.is_empty() {
121 return Err(BridgeError::InvalidInput(
122 "XFCC header parsed to zero entries".into(),
123 ));
124 }
125 for (i, e) in out.iter().enumerate() {
130 if e.uri.is_none() && e.subject.is_none() && e.by.is_none() {
131 return Err(BridgeError::InvalidInput(format!(
132 "XFCC entry #{} has no URI/By/Subject/DNS fields",
133 i
134 )));
135 }
136 }
137 Ok(out)
138}
139
140fn split_xfcc_entries(header: &str) -> Result<Vec<String>, BridgeError> {
143 let mut out = Vec::new();
144 let mut current = String::new();
145 let mut chars = header.chars().peekable();
146 let mut in_quotes = false;
147 while let Some(c) = chars.next() {
148 match c {
149 '\\' if in_quotes => {
150 current.push(c);
153 if let Some(next) = chars.next() {
154 current.push(next);
155 }
156 }
157 '"' => {
158 in_quotes = !in_quotes;
159 current.push(c);
160 }
161 ',' if !in_quotes => {
162 let trimmed = current.trim().to_string();
163 if !trimmed.is_empty() {
164 out.push(trimmed);
165 }
166 current.clear();
167 }
168 _ => current.push(c),
169 }
170 }
171 if in_quotes {
172 return Err(BridgeError::InvalidInput(
173 "XFCC header has mismatched quotes".into(),
174 ));
175 }
176 let trimmed = current.trim().to_string();
177 if !trimmed.is_empty() {
178 out.push(trimmed);
179 }
180 Ok(out)
181}
182
183fn parse_xfcc_entry(entry: &str) -> Result<XfccEntry, BridgeError> {
186 let pairs = split_xfcc_pairs(entry)?;
187 let mut out = XfccEntry::default();
188 let mut dns: Vec<String> = Vec::new();
189 for (k, v) in pairs {
190 match k.to_ascii_lowercase().as_str() {
191 "uri" => out.uri = Some(v),
192 "hash" => out.hash = Some(v),
193 "by" => out.by = Some(v),
194 "subject" => out.subject = Some(v),
195 "dns" => dns.push(v),
196 "cert" | "chain" => {}
200 _ => {}
202 }
203 }
204 if out.subject.is_none() && !dns.is_empty() {
206 out.subject = Some(format!("dns:{}", dns.join(",")));
207 }
208 Ok(out)
209}
210
211fn split_xfcc_pairs(entry: &str) -> Result<Vec<(String, String)>, BridgeError> {
215 let mut out = Vec::new();
216 let mut current = String::new();
217 let mut chars = entry.chars().peekable();
218 let mut in_quotes = false;
219 while let Some(c) = chars.next() {
220 match c {
221 '\\' if in_quotes => {
222 current.push(c);
223 if let Some(next) = chars.next() {
224 current.push(next);
225 }
226 }
227 '"' => {
228 in_quotes = !in_quotes;
229 current.push(c);
230 }
231 ';' if !in_quotes => {
232 push_pair(&mut out, ¤t)?;
233 current.clear();
234 }
235 _ => current.push(c),
236 }
237 }
238 if in_quotes {
239 return Err(BridgeError::InvalidInput(
240 "XFCC entry has mismatched quotes".into(),
241 ));
242 }
243 push_pair(&mut out, ¤t)?;
244 Ok(out)
245}
246
247fn push_pair(out: &mut Vec<(String, String)>, raw: &str) -> Result<(), BridgeError> {
248 let raw = raw.trim();
249 if raw.is_empty() {
250 return Ok(());
251 }
252 let eq = raw
253 .find('=')
254 .ok_or_else(|| BridgeError::InvalidInput(format!("XFCC pair missing '=': {}", raw)))?;
255 let key = raw[..eq].trim().to_string();
256 if key.is_empty() {
257 return Err(BridgeError::InvalidInput("XFCC pair has empty key".into()));
258 }
259 let value = unquote_xfcc_value(raw[eq + 1..].trim())?;
260 out.push((key, value));
261 Ok(())
262}
263
264fn unquote_xfcc_value(raw: &str) -> Result<String, BridgeError> {
266 if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
267 let inner = &raw[1..raw.len() - 1];
268 let mut out = String::with_capacity(inner.len());
269 let mut chars = inner.chars().peekable();
270 while let Some(c) = chars.next() {
271 if c == '\\' {
272 match chars.next() {
273 Some(esc @ ('"' | '\\')) => out.push(esc),
274 Some(other) => {
275 out.push('\\');
277 out.push(other);
278 }
279 None => {
280 return Err(BridgeError::InvalidInput(
281 "XFCC value ends with dangling backslash".into(),
282 ))
283 }
284 }
285 } else if c == '"' {
286 return Err(BridgeError::InvalidInput(
287 "XFCC value contains unescaped quote".into(),
288 ));
289 } else {
290 out.push(c);
291 }
292 }
293 Ok(out)
294 } else if raw.contains('"') {
295 Err(BridgeError::InvalidInput(
296 "XFCC value has mismatched quotes".into(),
297 ))
298 } else {
299 Ok(raw.to_string())
300 }
301}
302
303pub fn parse_istio_attributes(header: &str) -> Result<IstioPrincipal, BridgeError> {
318 let header = header.trim();
319 if header.is_empty() {
320 return Err(BridgeError::InvalidInput("empty Istio header".into()));
321 }
322 if let Some(p) = try_parse_istio_jwt(header)? {
326 return Ok(p);
327 }
328 let bytes = decode_base64_either(header)
331 .ok_or_else(|| BridgeError::InvalidInput("Istio header is not base64 or a JWT".into()))?;
332 let principal = decode_istio_protobuf_principal(&bytes)?;
333 spiffe_to_principal(&principal)
334}
335
336fn try_parse_istio_jwt(header: &str) -> Result<Option<IstioPrincipal>, BridgeError> {
340 let header = header.strip_prefix("Bearer ").unwrap_or(header).trim();
341 let parts: Vec<&str> = header.split('.').collect();
342 if parts.len() != 3 {
343 return Ok(None);
344 }
345 let payload_bytes = match URL_SAFE_NO_PAD.decode(parts[1].as_bytes()) {
347 Ok(v) => v,
348 Err(_) => return Ok(None),
349 };
350 let payload: Value = match serde_json::from_slice(&payload_bytes) {
351 Ok(v) => v,
352 Err(_) => return Ok(None),
353 };
354 let issuer = payload
359 .get("iss")
360 .and_then(Value::as_str)
361 .unwrap_or_default();
362 if !is_istio_issuer(issuer) {
363 return Err(BridgeError::Rejected(format!(
364 "Istio JWT has non-Istio issuer: {}",
365 issuer
366 )));
367 }
368 let spiffe = payload
371 .get("sub")
372 .and_then(Value::as_str)
373 .or_else(|| payload.get("spiffe").and_then(Value::as_str))
374 .ok_or_else(|| BridgeError::InvalidInput("Istio JWT missing sub/spiffe claim".into()))?;
375 Ok(Some(spiffe_to_principal(spiffe)?))
376}
377
378fn is_istio_issuer(iss: &str) -> bool {
379 matches!(
381 iss,
382 "https://kubernetes.default.svc.cluster.local"
383 | "kubernetes/serviceaccount"
384 | "istio-ca"
385 | "istiod.istio-system.svc"
386 ) || iss.starts_with("https://kubernetes.default.svc")
387 || iss.starts_with("istiod.")
388}
389
390fn decode_istio_protobuf_principal(bytes: &[u8]) -> Result<String, BridgeError> {
397 let mut i = 0;
398 let mut best: Option<String> = None;
399 while i < bytes.len() {
400 let (tag, n) = read_varint(&bytes[i..])
401 .ok_or_else(|| BridgeError::InvalidInput("Istio proto: bad varint tag".into()))?;
402 i += n;
403 let wire = (tag & 0x7) as u8;
404 match wire {
405 0 => {
406 let (_, n) = read_varint(&bytes[i..])
408 .ok_or_else(|| BridgeError::InvalidInput("Istio proto: bad varint".into()))?;
409 i += n;
410 }
411 1 => {
412 if bytes.len() < i + 8 {
414 return Err(BridgeError::InvalidInput(
415 "Istio proto: truncated fixed64".into(),
416 ));
417 }
418 i += 8;
419 }
420 2 => {
421 let (len, n) = read_varint(&bytes[i..]).ok_or_else(|| {
423 BridgeError::InvalidInput("Istio proto: bad length-delim varint".into())
424 })?;
425 i += n;
426 let len = len as usize;
427 if bytes.len() < i + len {
428 return Err(BridgeError::InvalidInput(
429 "Istio proto: truncated length-delim".into(),
430 ));
431 }
432 let payload = &bytes[i..i + len];
433 i += len;
434 if let Ok(s) = std::str::from_utf8(payload) {
437 if s.starts_with("spiffe://") && best.is_none() {
438 best = Some(s.to_string());
439 }
440 }
441 }
442 5 => {
443 if bytes.len() < i + 4 {
444 return Err(BridgeError::InvalidInput(
445 "Istio proto: truncated fixed32".into(),
446 ));
447 }
448 i += 4;
449 }
450 other => {
451 return Err(BridgeError::InvalidInput(format!(
452 "Istio proto: unknown wire type {}",
453 other
454 )));
455 }
456 }
457 }
458 best.ok_or_else(|| {
459 BridgeError::InvalidInput("Istio proto: no spiffe:// principal field present".into())
460 })
461}
462
463fn read_varint(bytes: &[u8]) -> Option<(u64, usize)> {
464 let mut result: u64 = 0;
465 let mut shift = 0u32;
466 for (i, b) in bytes.iter().enumerate() {
467 if i >= 10 {
468 return None;
469 }
470 result |= ((b & 0x7f) as u64) << shift;
471 if b & 0x80 == 0 {
472 return Some((result, i + 1));
473 }
474 shift += 7;
475 }
476 None
477}
478
479fn decode_base64_either(s: &str) -> Option<Vec<u8>> {
480 if let Ok(v) = STANDARD.decode(s.as_bytes()) {
481 return Some(v);
482 }
483 URL_SAFE_NO_PAD.decode(s.as_bytes()).ok()
484}
485
486fn spiffe_to_principal(spiffe: &str) -> Result<IstioPrincipal, BridgeError> {
487 let parsed: ParsedSpiffeId = parse_spiffe_id(spiffe)?;
488 let segments: Vec<&str> = parsed.path.split('/').collect();
490 let mut namespace = String::new();
491 let mut i = 0;
492 while i + 1 < segments.len() {
493 if segments[i] == "ns" {
494 namespace = segments[i + 1].to_string();
495 break;
496 }
497 i += 1;
498 }
499 if namespace.is_empty() {
500 return Err(BridgeError::InvalidInput(format!(
501 "Istio SPIFFE id has no /ns/<namespace>/ segment: {}",
502 spiffe
503 )));
504 }
505 Ok(IstioPrincipal {
506 spiffe_id: spiffe.to_string(),
507 namespace,
508 })
509}
510
511pub fn parse_linkerd_client_id(header: &str) -> Result<LinkerdClient, BridgeError> {
520 let header = header.trim();
521 if header.is_empty() {
522 return Err(BridgeError::InvalidInput(
523 "empty l5d-client-id header".into(),
524 ));
525 }
526 if header.starts_with("spiffe://") {
527 parse_spiffe_id(header)?;
529 return Ok(LinkerdClient {
530 spiffe_id: header.to_string(),
531 });
532 }
533 if header.contains("://") {
534 return Err(BridgeError::InvalidInput(format!(
535 "l5d-client-id has non-spiffe scheme: {}",
536 header
537 )));
538 }
539 let suffix = ".serviceaccount.identity.";
542 let idx = header.find(suffix).ok_or_else(|| {
543 BridgeError::InvalidInput(format!(
544 "l5d-client-id has no `.serviceaccount.identity.` segment: {}",
545 header
546 ))
547 })?;
548 let pre = &header[..idx];
549 let post = &header[idx + suffix.len()..];
550 let cluster = post.strip_suffix(".cluster.local").ok_or_else(|| {
551 BridgeError::InvalidInput(format!(
552 "l5d-client-id missing `.cluster.local` suffix: {}",
553 header
554 ))
555 })?;
556 let dot = pre.find('.').ok_or_else(|| {
557 BridgeError::InvalidInput(format!("l5d-client-id missing `<sa>.<ns>`: {}", header))
558 })?;
559 let sa = &pre[..dot];
560 let ns = &pre[dot + 1..];
561 if sa.is_empty() || ns.is_empty() || cluster.is_empty() {
562 return Err(BridgeError::InvalidInput(format!(
563 "l5d-client-id has empty sa/ns/cluster: {}",
564 header
565 )));
566 }
567 let synthetic = format!("spiffe://{}/ns/{}/sa/{}", cluster, ns, sa);
568 parse_spiffe_id(&synthetic)?;
569 Ok(LinkerdClient {
570 spiffe_id: synthetic,
571 })
572}
573
574pub fn envoy_accepted_event(entry: &XfccEntry) -> ProofEventStub {
580 ProofEventStub {
581 event_type: "bridge.service_mesh.envoy.accepted".into(),
582 payload: serde_json::json!({
583 "uri": entry.uri,
584 "by": entry.by,
585 "hash": entry.hash,
586 "subject": entry.subject,
587 }),
588 }
589}
590
591pub fn istio_accepted_event(p: &IstioPrincipal) -> ProofEventStub {
593 ProofEventStub {
594 event_type: "bridge.service_mesh.istio.accepted".into(),
595 payload: serde_json::json!({
596 "spiffe_id": p.spiffe_id,
597 "namespace": p.namespace,
598 }),
599 }
600}
601
602pub fn linkerd_accepted_event(c: &LinkerdClient) -> ProofEventStub {
604 ProofEventStub {
605 event_type: "bridge.service_mesh.linkerd.accepted".into(),
606 payload: serde_json::json!({ "spiffe_id": c.spiffe_id }),
607 }
608}
609
610impl ServiceMeshBridge {
615 pub fn new(cfg: ServiceMeshBridgeConfig) -> Self {
616 ServiceMeshBridge { cfg }
617 }
618
619 pub fn accept_envoy(&self, entry: &XfccEntry) -> Result<ActorIdentity, BridgeError> {
624 let uri = entry.uri.as_deref().ok_or_else(|| {
625 BridgeError::InvalidInput("XFCC entry needs URI in this Rust path".into())
626 })?;
627 if !uri.starts_with("spiffe://") {
628 return Err(BridgeError::Rejected(
629 "Rust XFCC bridge only accepts spiffe:// URIs".into(),
630 ));
631 }
632 let actor = spiffe_to_actor_id(uri)?;
633 Ok(self.identity_from(actor, entry.by.clone()))
634 }
635
636 pub fn accept_istio(&self, spiffe_id: &str) -> Result<ActorIdentity, BridgeError> {
637 if !spiffe_id.starts_with("spiffe://") {
638 return Err(BridgeError::InvalidInput(
639 "Istio context.spiffe_id must be a spiffe:// URI".into(),
640 ));
641 }
642 let actor = spiffe_to_actor_id(spiffe_id)?;
643 Ok(self.identity_from(actor, Some("istio".into())))
644 }
645
646 pub fn accept_linkerd(&self, client_id: &str) -> Result<ActorIdentity, BridgeError> {
647 if client_id.starts_with("spiffe://") {
650 let actor = spiffe_to_actor_id(client_id)?;
651 return Ok(self.identity_from(actor, Some("linkerd".into())));
652 }
653 let suffix = ".serviceaccount.identity.";
654 let idx = client_id.find(suffix).ok_or_else(|| {
655 BridgeError::InvalidInput(format!("not a linkerd client_id: {}", client_id))
656 })?;
657 let pre = &client_id[..idx];
658 let post = &client_id[idx + suffix.len()..];
659 let cluster_local = post.strip_suffix(".cluster.local").ok_or_else(|| {
660 BridgeError::InvalidInput(format!("not a linkerd client_id: {}", client_id))
661 })?;
662 let dot = pre.find('.').ok_or_else(|| {
663 BridgeError::InvalidInput(format!("not a linkerd client_id: {}", client_id))
664 })?;
665 let sa = &pre[..dot];
666 let ns = &pre[dot + 1..];
667 let actor = format!("tf:actor:service:{}/{}/{}", cluster_local, ns, sa);
668 Ok(self.identity_from(actor, Some("linkerd".into())))
669 }
670
671 fn identity_from(&self, actor: String, federation: Option<String>) -> ActorIdentity {
672 ActorIdentity {
673 identity_version: ActorIdentity_IdentityVersion::V1,
674 actor_id: actor,
675 actor_type: ActorType::Service,
676 instance_id: None,
677 public_keys: vec![PublicKey {
678 key_id: "service-mesh".into(),
679 algorithm: "ed25519".into(),
680 public_key: "AA==".into(),
681 purpose: PublicKey_Purpose::Signing,
682 valid_from: None,
683 valid_until: None,
684 }],
685 trust_levels: vec![TrustLevel::T3],
686 authority_roots: vec![AuthorityRoot {
687 kind: AuthorityRoot_Kind::Federation,
688 id: federation.unwrap_or_else(|| "service-mesh".into()),
689 }],
690 attestations: None,
691 valid_from: now_iso8601(),
692 valid_until: None,
693 revocation_ref: None,
694 signature: None,
695 }
696 }
697}
698
699impl Bridge for ServiceMeshBridge {
700 fn bridge_id(&self) -> &str {
701 &self.cfg.bridge_id
702 }
703 fn kind(&self) -> BridgeKind {
704 BridgeKind::ServiceMesh
705 }
706 fn trust_domain(&self) -> &str {
707 &self.cfg.trust_domain
708 }
709}
710
711fn now_iso8601() -> String {
712 let secs = std::time::SystemTime::now()
713 .duration_since(std::time::UNIX_EPOCH)
714 .unwrap_or_default()
715 .as_secs() as i64;
716 let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
717 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
718}
719
720fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
721 let days = secs.div_euclid(86_400);
722 let time = secs.rem_euclid(86_400);
723 let hour = (time / 3600) as u32;
724 let minute = ((time % 3600) / 60) as u32;
725 let second = (time % 60) as u32;
726 let z = days + 719_468;
727 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
728 let doe = (z - era * 146_097) as u64;
729 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
730 let y = yoe as i64 + era * 400;
731 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
732 let mp = (5 * doy + 2) / 153;
733 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
734 let m = if mp < 10 {
735 (mp + 3) as u32
736 } else {
737 (mp - 9) as u32
738 };
739 let year = if m <= 2 { y + 1 } else { y };
740 (year as i32, m, d, hour, minute, second)
741}