httpsig/
signature_base.rs

1use crate::{
2  crypto::SigningKey,
3  error::{HttpSigError, HttpSigResult},
4  message_component::HttpMessageComponent,
5  prelude::{message_component::HttpMessageComponentId, VerifyingKey},
6  signature_params::HttpSignatureParams,
7};
8use base64::{engine::general_purpose, Engine as _};
9use fxhash::FxBuildHasher;
10use indexmap::IndexMap;
11use sfv::{BareItem, Item, ListEntry, Parser};
12
13/// IndexMap of signature name and HttpSignatureHeaders
14pub type HttpSignatureHeadersMap = IndexMap<String, HttpSignatureHeaders, FxBuildHasher>;
15
16/// Default signature name used to indicate signature in http header (`signature` and `signature-input`)
17const DEFAULT_SIGNATURE_NAME: &str = "sig";
18
19#[derive(Debug, Clone)]
20/// Signature Headers derived from HttpSignatureBase
21pub struct HttpSignatureHeaders {
22  /// signature name coupling signature with signature input
23  signature_name: String,
24  /// Signature value of "Signature" http header in the form of "<signature_name>=:<base64_signature>:"
25  signature: HttpSignature,
26  /// signature-params value of "Signature-Input" http header in the form of "<signature_name>=:<signature_params>:"
27  signature_params: HttpSignatureParams,
28}
29
30impl HttpSignatureHeaders {
31  /// Generates (possibly multiple) HttpSignatureHeaders from signature and signature-input header values
32  pub fn try_parse(signature_header: &str, signature_input_header: &str) -> HttpSigResult<HttpSignatureHeadersMap> {
33    let signature_input =
34      Parser::parse_dictionary(signature_input_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?;
35    let signature =
36      Parser::parse_dictionary(signature_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?;
37
38    if signature.len() != signature_input.len() {
39      return Err(HttpSigError::BuildSignatureHeaderError(
40        "The number of signature and signature-input headers are not the same".to_string(),
41      ));
42    }
43
44    if !signature.keys().all(|k| signature_input.contains_key(k)) {
45      return Err(HttpSigError::BuildSignatureHeaderError(
46        "The signature and signature-input headers are not the same".to_string(),
47      ));
48    }
49    if !signature.values().all(|v| {
50      matches!(
51        v,
52        ListEntry::Item(Item {
53          bare_item: BareItem::ByteSeq(_),
54          ..
55        })
56      )
57    }) {
58      return Err(HttpSigError::BuildSignatureHeaderError(
59        "The signature header is not a dictionary".to_string(),
60      ));
61    }
62    if !signature_input.values().all(|v| matches!(v, ListEntry::InnerList(_))) {
63      return Err(HttpSigError::BuildSignatureHeaderError(
64        "The signature-input header is not a dictionary".to_string(),
65      ));
66    }
67
68    let res = signature_input
69      .iter()
70      .map(|(k, v)| {
71        let signature_name = k.to_string();
72        let signature_params = HttpSignatureParams::try_from(v)?;
73
74        let signature_bytes = match signature.get(k) {
75          Some(ListEntry::Item(Item {
76            bare_item: BareItem::ByteSeq(v),
77            ..
78          })) => v,
79          _ => unreachable!(),
80        };
81        let signature = HttpSignature(signature_bytes.to_vec());
82
83        Ok((
84          signature_name.clone(),
85          Self {
86            signature_name,
87            signature,
88            signature_params,
89          },
90        )) as HttpSigResult<(String, Self)>
91      })
92      .collect::<Result<HttpSignatureHeadersMap, _>>()?;
93    Ok(res)
94  }
95
96  /// Returns the signature name
97  pub fn signature_name(&self) -> &str {
98    &self.signature_name
99  }
100
101  /// Returns the signature value without name
102  pub fn signature(&self) -> &HttpSignature {
103    &self.signature
104  }
105
106  /// Returns the signature params value without name for signature-input header
107  pub fn signature_params(&self) -> &HttpSignatureParams {
108    &self.signature_params
109  }
110
111  /// Returns the signature value of "Signature" http header in the form of "<signature_name>=:<base64_signature>:"
112  pub fn signature_header_value(&self) -> String {
113    format!("{}=:{}:", self.signature_name, self.signature)
114  }
115  /// Returns the signature input value of "Signature-Input" http header in the form of "<signature_name>=<signature_params>"
116  pub fn signature_input_header_value(&self) -> String {
117    format!("{}={}", self.signature_name, self.signature_params)
118  }
119}
120
121#[derive(Debug, Clone)]
122/// Wrapper struct of raw signature bytes
123pub struct HttpSignature(Vec<u8>);
124impl std::fmt::Display for HttpSignature {
125  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126    let signature_value = general_purpose::STANDARD.encode(&self.0);
127    write!(f, "{}", signature_value)
128  }
129}
130
131/// Signature Base
132/// https://datatracker.ietf.org/doc/html/rfc9421#section-2.5
133pub struct HttpSignatureBase {
134  /// HTTP message field and derived components ordered as in the vector in signature params
135  component_lines: Vec<HttpMessageComponent>,
136  /// signature params
137  signature_params: HttpSignatureParams,
138}
139
140impl HttpSignatureBase {
141  /// Creates a new signature base from component lines and signature params
142  /// This should not be exposed to user and not used directly.
143  /// Use wrapper functions generating SignatureBase from base HTTP request and Signer itself instead when newly generating signature
144  /// When verifying signature, use wrapper functions generating SignatureBase from HTTP request containing signature params itself instead.
145  pub fn try_new(component_lines: &[HttpMessageComponent], signature_params: &HttpSignatureParams) -> HttpSigResult<Self> {
146    // check if the order of component lines is the same as the order of covered message component ids
147    if component_lines.len() != signature_params.covered_components.len() {
148      return Err(HttpSigError::BuildSignatureBaseError(
149        "The number of component lines is not the same as the number of covered message component ids".to_string(),
150      ));
151    }
152
153    let assertion = component_lines
154      .iter()
155      .zip(signature_params.covered_components.iter())
156      .all(|(component_line, covered_component_id)| component_line.id == *covered_component_id);
157    if !assertion {
158      return Err(HttpSigError::BuildSignatureBaseError(
159        "The order of component lines is not the same as the order of covered message component ids".to_string(),
160      ));
161    }
162
163    Ok(Self {
164      component_lines: component_lines.to_vec(),
165      signature_params: signature_params.clone(),
166    })
167  }
168
169  /// Returns the signature base string as bytes to be signed
170  pub fn as_bytes(&self) -> Vec<u8> {
171    let string = self.to_string();
172    string.as_bytes().to_vec()
173  }
174
175  /// Build signature from given signing key
176  pub fn build_raw_signature(&self, signing_key: &impl SigningKey) -> HttpSigResult<Vec<u8>> {
177    let bytes = self.as_bytes();
178    signing_key.sign(&bytes)
179  }
180
181  /// Build the signature and signature-input headers structs
182  pub fn build_signature_headers(
183    &self,
184    signing_key: &impl SigningKey,
185    signature_name: Option<&str>,
186  ) -> HttpSigResult<HttpSignatureHeaders> {
187    let signature = self.build_raw_signature(signing_key)?;
188    Ok(HttpSignatureHeaders {
189      signature_name: signature_name.unwrap_or(DEFAULT_SIGNATURE_NAME).to_string(),
190      signature: HttpSignature(signature),
191      signature_params: self.signature_params.clone(),
192    })
193  }
194
195  /// Verify the signature using the given verifying key
196  pub fn verify_signature_headers(
197    &self,
198    verifying_key: &impl VerifyingKey,
199    signature_headers: &HttpSignatureHeaders,
200  ) -> HttpSigResult<()> {
201    if signature_headers.signature_params().is_expired() {
202      return Err(HttpSigError::ExpiredSignatureParams(
203        "Signature params is expired".to_string(),
204      ));
205    }
206    let signature_bytes = signature_headers.signature.0.as_slice();
207    verifying_key.verify(&self.as_bytes(), signature_bytes)
208  }
209
210  /// Get key id from signature params
211  pub fn keyid(&self) -> Option<&str> {
212    self.signature_params.keyid.as_deref()
213  }
214
215  /// Get algorithm from signature params
216  pub fn alg(&self) -> Option<&str> {
217    self.signature_params.alg.as_deref()
218  }
219
220  /// Get nonce from signature params
221  pub fn nonce(&self) -> Option<&str> {
222    self.signature_params.nonce.as_deref()
223  }
224
225  /// Get covered components from signature params
226  pub fn covered_components(&self) -> &[HttpMessageComponentId] {
227    &self.signature_params.covered_components
228  }
229}
230
231impl std::fmt::Display for HttpSignatureBase {
232  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233    let mut signature_base = String::new();
234    for component_line in &self.component_lines {
235      signature_base.push_str(&component_line.to_string());
236      signature_base.push('\n');
237    }
238    signature_base.push_str(&format!("\"@signature-params\": {}", self.signature_params));
239    write!(f, "{}", signature_base)
240  }
241}
242
243#[cfg(test)]
244mod test {
245  use super::*;
246  use crate::signature_params::HttpSignatureParams;
247
248  const COMPONENT_LINES: &[&str] = &[
249    r##""@method": GET"##,
250    r##""@path": /"##,
251    r##""date": Tue, 07 Jun 2014 20:51:35 GMT"##,
252    r##""content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:"##,
253  ];
254
255  /// こんな感じでSignatureBaseをParamsとかComponentLinesから直接作るのは避ける。
256  #[test]
257  fn test_signature_base_directly_instantiated() {
258    const SIGPARA: &str = r##";created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""##;
259    let values = (r##""@method" "@path" "date" "content-digest""##, SIGPARA);
260    let signature_params = HttpSignatureParams::try_from(format!("({}){}", values.0, values.1).as_str()).unwrap();
261
262    let component_lines = COMPONENT_LINES
263      .iter()
264      .map(|&s| HttpMessageComponent::try_from(s))
265      .collect::<Result<Vec<_>, _>>()
266      .unwrap();
267    let signature_base = HttpSignatureBase::try_new(&component_lines, &signature_params).unwrap();
268    let test_string = r##""@method": GET
269"@path": /
270"date": Tue, 07 Jun 2014 20:51:35 GMT
271"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
272"@signature-params": "##;
273    assert_eq!(
274      signature_base.to_string(),
275      format!("{}({}){}", test_string, values.0, values.1)
276    );
277  }
278
279  #[test]
280  fn test_signature_values() {
281    const SIGNATURE_INPUT: &str = r##"sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519", sig-b27=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519-alt""##;
282    const SIGNATURE: &str = r##"sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:, sig-b27=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##;
283
284    let header_map = HttpSignatureHeaders::try_parse(SIGNATURE, SIGNATURE_INPUT).unwrap();
285    assert!(header_map.len() == 2);
286    let http_signature_headers = header_map.get("sig-b26").unwrap();
287    assert_eq!(
288      http_signature_headers.signature_header_value(),
289      SIGNATURE.split(',').next().unwrap()
290    );
291    assert_eq!(
292      http_signature_headers.signature_input_header_value(),
293      SIGNATURE_INPUT.split(',').next().unwrap()
294    );
295  }
296}