cratestack_core/
envelope.rs1mod keys;
11
12#[cfg(test)]
13mod tests;
14
15use std::sync::Arc;
16
17use serde::{Deserialize, Serialize};
18
19use crate::error::CoolError;
20
21pub use keys::{InMemoryNonceStore, KeyProvider, NonceStore, StaticKeyProvider};
22
23const ENVELOPE_DEFAULT_CLOCK_SKEW_SECS: i64 = 300;
28
29#[derive(Clone)]
33pub struct HmacEnvelope<K: KeyProvider> {
34 keys: Arc<K>,
35 signing_kid: String,
36 clock_skew_secs: i64,
37 nonces: Option<Arc<dyn NonceStore>>,
38}
39
40impl<K: KeyProvider> HmacEnvelope<K> {
41 pub fn new(keys: Arc<K>, signing_kid: impl Into<String>) -> Self {
42 Self {
43 keys,
44 signing_kid: signing_kid.into(),
45 clock_skew_secs: ENVELOPE_DEFAULT_CLOCK_SKEW_SECS,
46 nonces: None,
47 }
48 }
49
50 pub fn with_clock_skew_secs(mut self, secs: i64) -> Self {
51 self.clock_skew_secs = secs;
52 self
53 }
54
55 pub fn with_nonce_store(mut self, store: Arc<dyn NonceStore>) -> Self {
60 self.nonces = Some(store);
61 self
62 }
63
64 async fn compute_mac(&self, key: &[u8], input: &[u8]) -> Result<Vec<u8>, CoolError> {
65 use hmac::{Hmac, Mac};
66 let mut mac = <Hmac<sha2::Sha256> as Mac>::new_from_slice(key)
67 .map_err(|_| CoolError::Internal("HMAC key length error".to_owned()))?;
68 mac.update(input);
69 Ok(mac.finalize().into_bytes().to_vec())
70 }
71
72 pub async fn seal(&self, payload: serde_json::Value) -> Result<SealedEnvelope, CoolError> {
76 let key = self.keys.resolve_signing_key(&self.signing_kid).await?;
77 let ts = chrono::Utc::now().timestamp();
78 let nonce = uuid::Uuid::new_v4().to_string();
79 let mut envelope = SealedEnvelope {
80 kid: self.signing_kid.clone(),
81 alg: "HS256".to_owned(),
82 ts,
83 nonce,
84 body: payload,
85 mac_b64: String::new(),
86 };
87 let input = envelope.signing_input()?;
88 let mac = self.compute_mac(&key, &input).await?;
89 use base64::Engine;
90 envelope.mac_b64 = base64::engine::general_purpose::STANDARD.encode(mac);
91 Ok(envelope)
92 }
93
94 pub async fn open(&self, envelope: &SealedEnvelope) -> Result<serde_json::Value, CoolError> {
99 if envelope.alg != "HS256" {
100 return Err(CoolError::Unauthorized(format!(
101 "unsupported envelope algorithm '{}'",
102 envelope.alg,
103 )));
104 }
105 let now = chrono::Utc::now().timestamp();
106 let drift = (now - envelope.ts).abs();
107 if drift > self.clock_skew_secs {
108 return Err(CoolError::Unauthorized(
109 "envelope timestamp outside accepted skew window".to_owned(),
110 ));
111 }
112 let key = self.keys.resolve_signing_key(&envelope.kid).await?;
113 let input = envelope.signing_input()?;
114 let expected = self.compute_mac(&key, &input).await?;
115 use base64::Engine;
116 let actual = base64::engine::general_purpose::STANDARD
117 .decode(&envelope.mac_b64)
118 .map_err(|_| CoolError::Unauthorized("envelope MAC is not base64".to_owned()))?;
119 if actual.len() != expected.len() {
120 return Err(CoolError::Unauthorized(
121 "envelope MAC has wrong length".to_owned(),
122 ));
123 }
124 use subtle::ConstantTimeEq;
125 if !bool::from(actual.as_slice().ct_eq(expected.as_slice())) {
126 return Err(CoolError::Unauthorized(
127 "envelope MAC verification failed".to_owned(),
128 ));
129 }
130 if let Some(nonces) = &self.nonces {
131 let expires_at = chrono::DateTime::<chrono::Utc>::from_timestamp(
132 envelope.ts + self.clock_skew_secs,
133 0,
134 )
135 .ok_or_else(|| CoolError::Unauthorized("envelope timestamp out of range".to_owned()))?;
136 let recorded = nonces.record_if_unseen(&envelope.nonce, expires_at).await?;
137 if !recorded {
138 return Err(CoolError::Unauthorized(
139 "envelope nonce replay detected".to_owned(),
140 ));
141 }
142 }
143 Ok(envelope.body.clone())
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SealedEnvelope {
149 pub kid: String,
150 pub alg: String,
151 pub ts: i64,
152 pub nonce: String,
153 pub body: serde_json::Value,
154 pub mac_b64: String,
155}
156
157impl SealedEnvelope {
158 pub(crate) fn signing_input(&self) -> Result<Vec<u8>, CoolError> {
159 let mut buf = Vec::with_capacity(256);
160 buf.extend_from_slice(self.kid.as_bytes());
161 buf.push(0);
162 buf.extend_from_slice(self.alg.as_bytes());
163 buf.push(0);
164 buf.extend_from_slice(&self.ts.to_be_bytes());
165 buf.push(0);
166 buf.extend_from_slice(self.nonce.as_bytes());
167 buf.push(0);
168 let body_bytes = serde_json::to_vec(&self.body)
173 .map_err(|error| CoolError::Codec(format!("encode envelope body: {error}")))?;
174 buf.extend_from_slice(&body_bytes);
175 Ok(buf)
176 }
177}