osproxy_server/
directive.rs1use std::sync::Arc;
18use std::time::Duration;
19
20use osproxy_core::{Clock, IndexName, PartitionId, PrincipalId};
21use osproxy_observe::{DiagLevel, DiagnosticsDirective, DirectiveMatch, DirectiveVerifier};
22use serde_json::Value;
23
24#[cfg(all(feature = "fips", feature = "non-fips"))]
29compile_error!(
30 "features `fips` and `non-fips` are mutually exclusive; build with \
31 `--no-default-features --features fips` for a FIPS artifact"
32);
33#[cfg(not(any(feature = "fips", feature = "non-fips")))]
34compile_error!("enable exactly one crypto provider feature: `fips` or `non-fips`");
35
36#[cfg(feature = "fips")]
40use aws_lc_rs::hmac;
41#[cfg(feature = "non-fips")]
42use ring::hmac;
43
44pub struct HmacDirectiveVerifier {
46 key: hmac::Key,
47 clock: Arc<dyn Clock>,
48}
49
50impl std::fmt::Debug for HmacDirectiveVerifier {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("HmacDirectiveVerifier")
54 .finish_non_exhaustive()
55 }
56}
57
58impl HmacDirectiveVerifier {
59 #[must_use]
62 pub fn new(secret: &[u8], clock: Arc<dyn Clock>) -> Self {
63 Self {
64 key: hmac::Key::new(hmac::HMAC_SHA256, secret),
65 clock,
66 }
67 }
68
69 fn to_directive(&self, payload: &[u8]) -> Option<DiagnosticsDirective> {
72 let v: Value = serde_json::from_slice(payload).ok()?;
73 let level = parse_level(v.get("level")?.as_str()?)?;
74 let exp = v.get("exp")?.as_u64()?;
75
76 let now_secs = self.clock.unix_nanos() / 1_000_000_000;
79 let remaining = exp.checked_sub(now_secs)?;
80 if remaining == 0 {
81 return None;
82 }
83 let expires_at = self
84 .clock
85 .now()
86 .saturating_add(Duration::from_secs(remaining));
87
88 let mut match_ = DirectiveMatch::all();
89 if let Some(t) = v.get("tenant").and_then(Value::as_str) {
90 match_ = match_.for_tenant(PartitionId::from(t));
91 }
92 if let Some(i) = v.get("index").and_then(Value::as_str) {
93 match_ = match_.for_index(IndexName::from(i));
94 }
95 if let Some(p) = v.get("principal").and_then(Value::as_str) {
96 match_ = match_.for_principal(PrincipalId::from(p));
97 }
98 let sample_per_mille = match v.get("sample_per_mille") {
102 None => 1000,
103 Some(n) => match n.as_u64() {
104 Some(n) if n <= 1000 => u16::try_from(n).unwrap_or(1000),
105 _ => return None,
106 },
107 };
108
109 Some(DiagnosticsDirective {
110 id: "x-debug-header".to_owned(),
112 match_,
113 level,
114 sample_per_mille,
115 expires_at,
116 ring_buffer: v
117 .get("ring_buffer")
118 .and_then(Value::as_bool)
119 .unwrap_or(false),
120 capture: v.get("capture").and_then(Value::as_bool).unwrap_or(false),
121 })
122 }
123}
124
125impl DirectiveVerifier for HmacDirectiveVerifier {
126 fn verify(&self, header_value: &str) -> Option<DiagnosticsDirective> {
127 let (payload_hex, sig_hex) = header_value.split_once('.')?;
128 let payload = decode_hex(payload_hex)?;
129 let sig = decode_hex(sig_hex)?;
130 hmac::verify(&self.key, &payload, &sig).ok()?;
132 self.to_directive(&payload)
133 }
134}
135
136pub(crate) fn parse_level(name: &str) -> Option<DiagLevel> {
140 DiagLevel::from_name(name)
141}
142
143fn decode_hex(s: &str) -> Option<Vec<u8>> {
146 if !s.len().is_multiple_of(2) {
147 return None;
148 }
149 let bytes = s.as_bytes();
150 let mut out = Vec::with_capacity(s.len() / 2);
151 let mut i = 0;
152 while i < bytes.len() {
153 let hi = (bytes[i] as char).to_digit(16)?;
154 let lo = (bytes[i + 1] as char).to_digit(16)?;
155 out.push(u8::try_from(hi * 16 + lo).ok()?);
156 i += 2;
157 }
158 Some(out)
159}
160
161#[cfg(test)]
164#[must_use]
165pub(crate) fn encode_hex(bytes: &[u8]) -> String {
166 let mut out = String::with_capacity(bytes.len() * 2);
167 for &b in bytes {
168 out.push(char::from_digit(u32::from(b >> 4), 16).unwrap_or('0'));
169 out.push(char::from_digit(u32::from(b & 0x0f), 16).unwrap_or('0'));
170 }
171 out
172}
173
174#[cfg(test)]
177#[must_use]
178pub(crate) fn sign_token(secret: &[u8], payload: &[u8]) -> String {
179 let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
180 let tag = hmac::sign(&key, payload);
181 format!("{}.{}", encode_hex(payload), encode_hex(tag.as_ref()))
182}
183
184#[cfg(test)]
185#[path = "directive_tests.rs"]
186mod tests;