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