httpsig/
signature_params.rs

1use crate::{
2  crypto::{AlgorithmName, SigningKey},
3  error::{HttpSigError, HttpSigResult},
4  message_component::HttpMessageComponentId,
5  trace::*,
6  util::has_unique_elements,
7};
8use base64::{engine::general_purpose, Engine as _};
9use rand::Rng;
10use sfv::{ListEntry, Parser, SerializeValue};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13const DEFAULT_DURATION: u64 = 300;
14
15/* ---------------------------------------- */
16#[derive(Debug, Clone, Default)]
17/// Struct defining Http message signature parameters
18/// https://datatracker.ietf.org/doc/html/rfc9421#name-signature-parameters
19pub struct HttpSignatureParams {
20  /// created unix timestamp.
21  pub created: Option<u64>,
22  /// signature expires unix timestamp.
23  pub expires: Option<u64>,
24  /// nonce
25  pub nonce: Option<String>,
26  /// algorithm name
27  pub alg: Option<String>,
28  /// key id.
29  pub keyid: Option<String>,
30  /// tag
31  pub tag: Option<String>,
32  /// covered component vector string: ordered message components, i.e., string of http_fields and derived_components
33  pub covered_components: Vec<HttpMessageComponentId>,
34}
35
36impl HttpSignatureParams {
37  /// Create new HttpSignatureParams object for the given covered components only with `created`` current timestamp.
38  pub fn try_new(covered_components: &[HttpMessageComponentId]) -> HttpSigResult<Self> {
39    let created = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
40    if !has_unique_elements(covered_components.iter()) {
41      return Err(HttpSigError::InvalidSignatureParams(
42        "duplicate covered component ids".to_string(),
43      ));
44    }
45
46    Ok(Self {
47      created: Some(created),
48      covered_components: covered_components.to_vec(),
49      ..Default::default()
50    })
51  }
52
53  /// Set artificial `created` timestamp
54  pub fn set_created(&mut self, created: u64) -> &mut Self {
55    self.created = Some(created);
56    self
57  }
58
59  /// Set `expires` timestamp
60  pub fn set_expires(&mut self, expires: u64) -> &mut Self {
61    self.expires = Some(expires);
62    self
63  }
64
65  /// Set `nonce`
66  pub fn set_nonce(&mut self, nonce: &str) -> &mut Self {
67    self.nonce = Some(nonce.to_string());
68    self
69  }
70
71  /// Set `alg`
72  pub fn set_alg(&mut self, alg: &AlgorithmName) -> &mut Self {
73    self.alg = Some(alg.to_string());
74    self
75  }
76
77  /// Set `keyid`
78  pub fn set_keyid(&mut self, keyid: &str) -> &mut Self {
79    self.keyid = Some(keyid.to_string());
80    self
81  }
82
83  /// Set `tag`
84  pub fn set_tag(&mut self, tag: &str) -> &mut Self {
85    self.tag = Some(tag.to_string());
86    self
87  }
88
89  /// Set `keyid` and `alg` from the signing key
90  pub fn set_key_info(&mut self, key: &impl SigningKey) -> &mut Self {
91    self.keyid = Some(key.key_id().to_string());
92    self.alg = Some(key.alg().to_string());
93    self
94  }
95
96  /// Set random nonce
97  pub fn set_random_nonce(&mut self) -> &mut Self {
98    let mut rng = rand::rng();
99    let nonce = rng.random::<[u8; 32]>();
100    self.nonce = Some(general_purpose::STANDARD.encode(nonce));
101    self
102  }
103
104  /// Set `expires` timestamp from the current timestamp
105  pub fn set_expires_with_duration(&mut self, duration_secs: Option<u64>) -> &mut Self {
106    assert!(self.created.is_some(), "created timestamp is not set");
107    let duration_secs = duration_secs.unwrap_or(DEFAULT_DURATION);
108    self.expires = Some(self.created.unwrap() + duration_secs);
109    self
110  }
111
112  /// Check if the signature params is expired if `exp` field is present.
113  /// If `exp` field is not present, it always returns false.
114  pub fn is_expired(&self) -> bool {
115    if let Some(exp) = self.expires {
116      exp < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
117    } else {
118      false
119    }
120  }
121}
122
123impl std::fmt::Display for HttpSignatureParams {
124  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125    let joined = self.covered_components.iter().fold("".to_string(), |acc, v| {
126      if acc.is_empty() {
127        v.to_string()
128      } else {
129        format!("{acc} {v}")
130      }
131    });
132    let mut s: String = format!("({})", joined);
133    if self.created.is_some() {
134      s.push_str(&format!(";created={}", self.created.unwrap()));
135    }
136    if self.expires.is_some() {
137      s.push_str(&format!(";expires={}", self.expires.unwrap()));
138    }
139    if self.nonce.is_some() {
140      s.push_str(&format!(";nonce=\"{}\"", self.nonce.as_ref().unwrap()));
141    }
142    if self.alg.is_some() {
143      s.push_str(&format!(";alg=\"{}\"", self.alg.as_ref().unwrap()));
144    }
145    if self.keyid.is_some() {
146      s.push_str(&format!(";keyid=\"{}\"", self.keyid.as_ref().unwrap()));
147    }
148    if self.tag.is_some() {
149      s.push_str(&format!(";tag=\"{}\"", self.tag.as_ref().unwrap()));
150    }
151    write!(f, "{}", s)
152  }
153}
154
155impl TryFrom<&ListEntry> for HttpSignatureParams {
156  type Error = HttpSigError;
157  /// Convert from ListEntry to HttpSignatureParams
158  fn try_from(value: &ListEntry) -> HttpSigResult<Self> {
159    if !matches!(value, ListEntry::InnerList(_)) {
160      return Err(HttpSigError::InvalidSignatureParams("Invalid signature params".to_string()));
161    }
162    let inner_list_with_params = match value {
163      ListEntry::InnerList(v) => v,
164      _ => unreachable!(),
165    };
166    let covered_components = inner_list_with_params
167      .items
168      .iter()
169      .map(|v| {
170        v.serialize_value()
171          .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))
172          .and_then(|v| HttpMessageComponentId::try_from(v.as_str()))
173      })
174      .collect::<Result<Vec<_>, _>>()?;
175
176    if !has_unique_elements(covered_components.iter()) {
177      return Err(HttpSigError::InvalidSignatureParams(
178        "duplicate covered component ids".to_string(),
179      ));
180    }
181
182    let mut params = Self {
183      created: None,
184      expires: None,
185      nonce: None,
186      alg: None,
187      keyid: None,
188      tag: None,
189      covered_components,
190    };
191
192    inner_list_with_params
193      .params
194      .iter()
195      .for_each(|(key, bare_item)| match key.as_str() {
196        "created" => params.created = bare_item.as_int().map(|v| v as u64),
197        "expires" => params.expires = bare_item.as_int().map(|v| v as u64),
198        "nonce" => params.nonce = bare_item.as_str().map(|v| v.to_string()),
199        "alg" => params.alg = bare_item.as_str().map(|v| v.to_string()),
200        "keyid" => params.keyid = bare_item.as_str().map(|v| v.to_string()),
201        "tag" => params.tag = bare_item.as_str().map(|v| v.to_string()),
202        _ => {
203          error!("Ignore invalid signature parameter: {}", key)
204        }
205      });
206    Ok(params)
207  }
208}
209
210impl TryFrom<&str> for HttpSignatureParams {
211  type Error = HttpSigError;
212  /// Convert from string to HttpSignatureParams
213  fn try_from(value: &str) -> HttpSigResult<Self> {
214    let sfv_parsed = Parser::parse_list(value.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?;
215    if sfv_parsed.len() != 1 || !matches!(sfv_parsed[0], ListEntry::InnerList(_)) {
216      return Err(HttpSigError::InvalidSignatureParams("Invalid signature params".to_string()));
217    }
218    HttpSignatureParams::try_from(&sfv_parsed[0])
219  }
220}
221
222#[cfg(test)]
223mod tests {
224  use super::*;
225  use crate::crypto::SecretKey;
226  const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY-----
227MC4CAQAwBQYDK2VwBCIEIDSHAE++q1BP7T8tk+mJtS+hLf81B0o6CFyWgucDFN/C
228-----END PRIVATE KEY-----
229"##;
230  const _EDDSA_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY-----
231MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0=
232-----END PUBLIC KEY-----
233"##;
234  const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=";
235
236  fn build_covered_components() -> Vec<HttpMessageComponentId> {
237    vec![
238      HttpMessageComponentId::try_from("@method").unwrap(),
239      HttpMessageComponentId::try_from("@path").unwrap(),
240      HttpMessageComponentId::try_from("@scheme").unwrap(),
241      HttpMessageComponentId::try_from("@authority").unwrap(),
242      HttpMessageComponentId::try_from("content-type").unwrap(),
243      HttpMessageComponentId::try_from("date").unwrap(),
244      HttpMessageComponentId::try_from("content-length").unwrap(),
245    ]
246  }
247
248  #[test]
249  fn test_try_new() {
250    let params = HttpSignatureParams::try_new(&build_covered_components());
251    assert!(params.is_ok());
252    let params = params.unwrap();
253    assert!(params.created.is_some());
254    assert!(params.expires.is_none());
255    assert!(params.nonce.is_none());
256    assert!(params.alg.is_none());
257    assert!(params.keyid.is_none());
258    assert!(params.tag.is_none());
259    assert_eq!(params.covered_components.len(), 7);
260  }
261
262  #[test]
263  fn test_set_key_info() {
264    let mut params = HttpSignatureParams::try_new(&build_covered_components()).unwrap();
265    params.set_key_info(&SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap());
266    assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string()));
267    assert_eq!(params.alg, Some("ed25519".to_string()));
268  }
269
270  #[test]
271  fn test_set_duration() {
272    let mut params = HttpSignatureParams::try_new(&build_covered_components()).unwrap();
273    params.set_expires_with_duration(Some(100));
274    assert!(params.expires.is_some());
275    assert_eq!(params.expires.unwrap(), params.created.unwrap() + 100);
276    assert!(!params.is_expired());
277
278    let created = params.created.unwrap();
279    params.set_expires(created - 1);
280    assert!(params.is_expired());
281  }
282
283  #[test]
284  fn test_from_string_signature_params_without_param() {
285    let value = r##"("@method" "@path" "@scheme" "@authority" "content-type" "date" "content-length")"##;
286    let params = HttpSignatureParams::try_from(value);
287    assert!(params.is_ok());
288    let params = params.unwrap();
289    assert!(params.created.is_none());
290    assert!(params.expires.is_none());
291    assert!(params.nonce.is_none());
292    assert!(params.alg.is_none());
293    assert!(params.keyid.is_none());
294    assert!(params.tag.is_none());
295    assert_eq!(params.covered_components.len(), 7);
296  }
297
298  #[test]
299  fn test_from_string_signature_params() {
300    const SIGPARA: &str = r##";created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""##;
301    let values = vec![
302      (
303        r##""@method" "@path" "@scheme";req "@authority" "content-type";bs "date" "content-length""##,
304        SIGPARA,
305      ),
306      (r##""##, SIGPARA),
307    ];
308    for (covered, sigpara) in values {
309      let value = format!("({}){}", covered, sigpara);
310      let params = HttpSignatureParams::try_from(value.as_str());
311      assert!(params.is_ok());
312      let params = params.unwrap();
313
314      assert_eq!(params.created, Some(1704972031));
315      assert_eq!(params.expires, None);
316      assert_eq!(params.nonce, None);
317      assert_eq!(params.alg, Some("ed25519".to_string()));
318      assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string()));
319      assert_eq!(params.tag, None);
320      let covered_components = covered
321        .split(' ')
322        .filter(|v| !v.is_empty())
323        .map(|v| HttpMessageComponentId::try_from(v).unwrap())
324        .collect::<Vec<_>>();
325      assert_eq!(params.covered_components, covered_components);
326      assert_eq!(params.to_string(), value);
327    }
328  }
329}