Skip to main content

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