Skip to main content

httpsig_hyper/
lib.rs

1//! # httpsig-hyper
2//!
3//! `httpsig-hyper` is a crate that provides a convenient API for `Hyper` users to handle HTTP signatures.
4//! This crate extends hyper's http request and response messages with the ability to generate and verify HTTP signatures.
5//! Additionally it also provides a way to set and verify content-digest header.
6//!
7//! ## Async-first design
8//!
9//! The primary API is fully async, allowing concurrent processing of multiple signatures via
10//! [`MessageSignatureReq`] and [`MessageSignatureRes`].
11//!
12//! ## Blocking API
13//!
14//! When the `blocking` feature is enabled (on by default), synchronous wrappers are provided via
15//! [`MessageSignatureReqSync`] and [`MessageSignatureResSync`]. These use `futures::executor::block_on`
16//! internally and are intended **exclusively for non-async contexts**.
17//!
18//! # Panics
19//!
20//! Calling any `*_sync` method from within an async runtime (e.g. inside a `tokio::spawn` task)
21//! will panic. If you are already in an async context, use the async methods directly.
22
23mod error;
24mod hyper_content_digest;
25mod hyper_http;
26
27// hyper's http specific extension to generate and verify http signature
28
29/// content-digest header name
30const CONTENT_DIGEST_HEADER: &str = "content-digest";
31
32/// content-digest header type
33pub enum ContentDigestType {
34  Sha256,
35  Sha512,
36}
37
38impl std::fmt::Display for ContentDigestType {
39  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40    match self {
41      ContentDigestType::Sha256 => write!(f, "sha-256"),
42      ContentDigestType::Sha512 => write!(f, "sha-512"),
43    }
44  }
45}
46
47impl std::str::FromStr for ContentDigestType {
48  type Err = error::HyperDigestError;
49  fn from_str(s: &str) -> Result<Self, Self::Err> {
50    match s {
51      "sha-256" => Ok(ContentDigestType::Sha256),
52      "sha-512" => Ok(ContentDigestType::Sha512),
53      _ => Err(error::HyperDigestError::InvalidContentDigestType(s.to_string())),
54    }
55  }
56}
57
58pub use error::{HyperDigestError, HyperDigestResult, HyperSigError, HyperSigResult};
59pub use httpsig::prelude;
60pub use hyper_content_digest::{ContentDigest, RequestContentDigest, ResponseContentDigest};
61pub use hyper_http::{
62  MessageSignature, MessageSignatureReq, MessageSignatureReqSync, MessageSignatureRes, MessageSignatureResSync,
63};
64
65/* ----------------------------------------------------------------- */
66#[cfg(test)]
67mod tests {
68  use super::{prelude::*, *};
69  use http::{Request, Response};
70  use http_body_util::Full;
71  use httpsig::prelude::{PublicKey, SecretKey};
72
73  type BoxBody = http_body_util::combinators::BoxBody<bytes::Bytes, crate::error::HyperDigestError>;
74
75  const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY-----
76MC4CAQAwBQYDK2VwBCIEIDSHAE++q1BP7T8tk+mJtS+hLf81B0o6CFyWgucDFN/C
77-----END PRIVATE KEY-----
78"##;
79  const EDDSA_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY-----
80MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0=
81-----END PUBLIC KEY-----
82"##;
83  // const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is";
84
85  const COVERED_COMPONENTS_REQ: &[&str] = &["@method", "date", "content-type", "content-digest"];
86  const COVERED_COMPONENTS_RES: &[&str] = &["@status", "\"@method\";req", "date", "content-type", "\"content-digest\";req"];
87
88  async fn build_request() -> Request<BoxBody> {
89    let body = Full::new(&b"{\"hello\": \"world\"}"[..]);
90    let req = Request::builder()
91        .method("GET")
92        .uri("https://example.com/parameters?var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something")
93        .header("date", "Sun, 09 May 2021 18:30:00 GMT")
94        .header("content-type", "application/json")
95        .header("content-type", "application/json-patch+json")
96        .body(body)
97        .unwrap();
98    req.set_content_digest(&ContentDigestType::Sha256).await.unwrap()
99  }
100
101  async fn build_response() -> Response<BoxBody> {
102    let body = Full::new(&b"{\"hello\": \"world!!\"}"[..]);
103    let res = Response::builder()
104      .status(200)
105      .header("date", "Sun, 09 May 2021 18:30:00 GMT")
106      .header("content-type", "application/json")
107      .header("content-type", "application/json-patch+json")
108      .body(body)
109      .unwrap();
110    res.set_content_digest(&ContentDigestType::Sha256).await.unwrap()
111  }
112
113  #[test]
114  fn test_content_digest_type() {
115    assert_eq!(ContentDigestType::Sha256.to_string(), "sha-256");
116    assert_eq!(ContentDigestType::Sha512.to_string(), "sha-512");
117  }
118
119  #[tokio::test]
120  async fn test_set_verify_request() {
121    // show usage of set_message_signature and verify_message_signature
122
123    let mut req = build_request().await;
124
125    let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap();
126
127    let covered_components = COVERED_COMPONENTS_REQ
128      .iter()
129      .map(|v| message_component::HttpMessageComponentId::try_from(*v))
130      .collect::<Result<Vec<_>, _>>()
131      .unwrap();
132    let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap();
133
134    // set key information, alg and keyid
135    signature_params.set_key_info(&secret_key);
136
137    // set custom signature name
138    req
139      .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name"))
140      .await
141      .unwrap();
142    let signature_input = req.headers().get("signature-input").unwrap().to_str().unwrap();
143    let signature = req.headers().get("signature").unwrap().to_str().unwrap();
144    assert!(signature_input.starts_with(r##"custom_sig_name=("##));
145    assert!(signature.starts_with(r##"custom_sig_name=:"##));
146
147    // verify without checking key_id
148    // get algorithm from signature params
149    let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1;
150    let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap();
151    let verification_res = req.verify_message_signature(&public_key, None).await;
152    assert!(verification_res.is_ok());
153
154    // verify with checking key_id
155    let key_id = public_key.key_id();
156    let verification_res = req.verify_message_signature(&public_key, Some(&key_id)).await;
157    assert!(verification_res.is_ok());
158
159    let verification_res = req.verify_message_signature(&public_key, Some("NotFoundKeyId")).await;
160    assert!(verification_res.is_err());
161  }
162
163  #[tokio::test]
164  async fn test_set_verify_response() {
165    // show usage of set_message_signature and verify_message_signature
166
167    let req = build_request().await;
168    let mut res = build_response().await;
169
170    let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap();
171
172    let covered_components = COVERED_COMPONENTS_RES
173      .iter()
174      .map(|v| message_component::HttpMessageComponentId::try_from(*v))
175      .collect::<Result<Vec<_>, _>>()
176      .unwrap();
177    let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap();
178
179    // set key information, alg and keyid
180    signature_params.set_key_info(&secret_key);
181
182    // set custom signature name, and `req` field param if needed (e.g., request method, uri, content-digest, etc.) included only in response
183    res
184      .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name"), Some(&req))
185      .await
186      .unwrap();
187    let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap();
188    let signature = res.headers().get("signature").unwrap().to_str().unwrap();
189    assert!(signature_input.starts_with(r##"custom_sig_name=("##));
190    assert!(signature.starts_with(r##"custom_sig_name=:"##));
191
192    // verify without checking key_id, request must be provided if `req` field param is included in signature params
193    // get algorithm from signature params
194    let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1;
195    let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap();
196    let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await;
197    assert!(verification_res.is_ok());
198    let verification_res = res
199      .verify_message_signature(&public_key, None, None as Option<&Request<()>>)
200      .await;
201    assert!(verification_res.is_err());
202
203    // verify with checking key_id
204    let key_id = public_key.key_id();
205    let verification_res = res.verify_message_signature(&public_key, Some(&key_id), Some(&req)).await;
206    assert!(verification_res.is_ok());
207
208    let verification_res = res
209      .verify_message_signature(&public_key, Some("NotFoundKeyId"), Some(&req))
210      .await;
211    assert!(verification_res.is_err());
212  }
213
214  #[cfg(feature = "blocking")]
215  #[test]
216  fn test_set_verify_request_sync() {
217    // show usage of set_message_signature_sync and verify_message_signature_sync
218
219    let mut req = futures::executor::block_on(build_request());
220    let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap();
221    let covered_components = COVERED_COMPONENTS_REQ
222      .iter()
223      .map(|v| message_component::HttpMessageComponentId::try_from(*v))
224      .collect::<Result<Vec<_>, _>>()
225      .unwrap();
226    let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap();
227    // set key information, alg and keyid
228    signature_params.set_key_info(&secret_key);
229    // set signature
230    req.set_message_signature_sync(&signature_params, &secret_key, None).unwrap();
231
232    let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1;
233    let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap();
234    let verification_res = req.verify_message_signature_sync(&public_key, None);
235    assert!(verification_res.is_ok());
236  }
237
238  #[cfg(feature = "blocking")]
239  #[test]
240  fn test_set_verify_response_sync() {
241    // show usage of set_message_signature_sync and verify_message_signature_sync
242    let req = futures::executor::block_on(build_request());
243    let mut res = futures::executor::block_on(build_response());
244    let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap();
245    let covered_components = COVERED_COMPONENTS_RES
246      .iter()
247      .map(|v| message_component::HttpMessageComponentId::try_from(*v))
248      .collect::<Result<Vec<_>, _>>()
249      .unwrap();
250    let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap();
251    // set key information, alg and keyid
252    signature_params.set_key_info(&secret_key);
253    // set signature
254    res
255      .set_message_signature_sync(&signature_params, &secret_key, None, Some(&req))
256      .unwrap();
257
258    let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1;
259    let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap();
260    let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req));
261    assert!(verification_res.is_ok());
262  }
263}