Signing a json `reqwest.Request.body()`

I am trying to implement something like the Authenticating Requests: Using the Authorization Header (AWS Signature Version 4) or Signing HTTP Messages draft.

Basically what I want to do is to add several headers to my HTTP request, where one of them is a digest of the request's body. Then I can construct a string that sums that digest header a nonce and some other headers and cryptographically sign that sum.

Having a digest for JSON might be problematic, since the orders of keys in an object might not be guaranteed, and the white space can be added and removed. So I wish to sign the body, and not the JSON I want to add to that body.

This is however difficult when using Reqwest, not only that body is a private to RequestBuilder, and it has a write only interface for it, the Body object also does not expose it's content.

So I ended up adding a trait to the RequestBuilder:

pub trait JsonDigest {
    fn json_digest<T: Serialize + ?Sized>(
        self,
        json: &T,
        algorithm: &'static digest::Algorithm,
    ) -> Self;
}

impl JsonDigest for RequestBuilder {
    /// Sets the body to the JSON serialization of the passed value,
    /// and also sets the `Content-Type: application/json` header, and
    /// add a digest header encoded as hex.
    ///
    /// # Errors
    ///
    /// Serialization can fail if `T`'s implementation of `Serialize`
    /// decides to fail, or if `T` contains a map with non-string
    /// keys.
    fn json_digest<T: Serialize + ?Sized>(
        self,
        json: &T,
        algorithm: &'static digest::Algorithm,
    ) -> Self {
        match serde_json::to_vec(json) {
            Ok(json_bytes) => self
                .header(
                    HeaderName::from_static("digest"),
                    hex::encode(
                        digest::digest(algorithm, json_bytes.as_ref()).as_ref(),
                    )
                    .as_bytes(),
                )
                .header(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static("application/json"),
                )
                .body(json_bytes),
            // HACK: Since json serialization failed, the following
            //       should also fail in the same manner, and when it
            //       does it will change self.request to be an
            //       Err(...). Note that since that is a private
            //       member we can't access it directly.
            Err(_) => self.json(json),
        }
    }
}

On the response side I also encountered unforeseen difficulties, since Responce.json(), Responce.text(), and Responce.copy_to() are all consume the requests content. So I can't easily both verify the digest and parse the JSON.

So either I misunderstand something fundamental about the Reqwest crate, or I am using the wrong crate for my task. In both cases I would appreciate any advice.

So I used an other trait to overcome the limitation of reading the body content just once:

use serde::de::DeserializeOwned;
use serde_json;
use failoure

type Result<T> = std::io::Result<T, failure::Error>;

pub trait CheckedJson {
    fn checked_json<T: DeserializeOwned>(&mut self) -> Result<T>;
}

impl CheckedJson for Response {
    fn checked_json<T: DeserializeOwned>(&mut self) -> Result<T> {
        let mut buf = Vec::<u8>::with_capacity(
            self.content_length().unwrap_or(1024) as usize,
        );
        self.copy_to(&mut buf)?;
        let computed_digest = digest::digest(&digest::SHA256, &buf);
        let expected_digest = get_expected_digest_from_headers(&self);
        if computed_digest.as_ref() == expected_digest.as_slice() {
            Ok(serde_json::from_slice(&buf)?)
        } else {
            Err(failure::format_err!("digest verification"))
        }
    }
}

and while there might be a better way to do it, or a more suitable library this solution works for me.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.