http-signatures 0.8.0

An implementation of the HTTP Signatures RFC
// This file is part of HTTP Signatures

// HTTP Signatures is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// HTTP Signatures is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with HTTP Signatures  If not, see <http://www.gnu.org/licenses/>.

//! Available with the `use_reqwest` feature. This module defines `AsHttpSignature` and
//! `WithHttpSignature` for `reqwest::Request`.
//!
//! # Example adding a signature to a request
//!
//! This example adds the HTTP Signature to the request directly as an Authorization header.
//! `with_signature_header` can be used to add the signature as a Signature header instead.
//!
//! ```rust
//! # extern crate reqwest;
//! # extern crate http_signatures;
//! # extern crate ring;
//! #
//! # use std::{error::Error, fs::File, io::Read};
//! #
//! # use http_signatures::{prelude::*, CreateKey, Input, ShaSize, SignatureAlgorithm};
//! # use reqwest::Client;
//! # use ring::signature::RSAKeyPair;
//! #
//! # fn run() -> Result<(), Box<Error>> {
//! # let mut key_file = File::open("tests/assets/private.der")?;
//! # let mut key_vec = Vec::new();
//! # key_file.read_to_end(&mut key_vec)?;
//! #
//! # let key_input = Input::from(&key_vec);
//! # let key = RSAKeyPair::from_der(key_input)?;
//! let creation_key = CreateKey::rsa(key, ShaSize::SHA512);
//!
//! let client = Client::new();
//! let mut req = client.get("https://example.com").build()?;
//!
//! req.with_authorization_header("rsa-key-1".into(), creation_key)?;
//! #     Ok(())
//! # }
//! # fn main() {
//! #     run().unwrap();
//! # }
//! ```
//!
//! See
//! [this example](https://git.asonix.dog/asonix/http-signatures/src/branch/master/examples/reqwest-client)
//! for more usage information.

use std::collections::BTreeMap;

use crate::{
    create::{CreateKey, HttpSignature},
    error::{CreationError, Error},
    prelude::*,
    REQUEST_TARGET,
};

use reqwest::{header::HeaderValue, Request as ReqwestRequest};

impl AsHttpSignature for ReqwestRequest {
    fn as_http_signature<'a>(
        &self,
        key_id: String,
        key: CreateKey,
    ) -> Result<HttpSignature, Error> {
        let mut headers = BTreeMap::new();
        headers.insert(
            REQUEST_TARGET.into(),
            vec![if let Some(query) = self.url().query() {
                format!(
                    "{} {}?{}",
                    self.method().as_ref().to_lowercase(),
                    self.url().path(),
                    query
                )
            } else {
                format!(
                    "{} {}",
                    self.method().as_ref().to_lowercase(),
                    self.url().path()
                )
            }],
        );

        let headers =
            self.headers()
                .iter()
                .fold(headers, |mut acc, (header_name, header_value)| {
                    let _ = header_value.to_str().map(|header_value| {
                        acc.entry(header_name.as_str().to_string())
                            .or_insert_with(Vec::new)
                            .push(header_value.to_string());
                    });

                    acc
                });

        HttpSignature::new(key_id, key, headers).map_err(Error::from)
    }
}

impl WithHttpSignature for ReqwestRequest {
    fn with_authorization_header(
        &mut self,
        key_id: String,
        key: CreateKey,
    ) -> Result<&mut Self, Error> {
        use reqwest::header::AUTHORIZATION;

        let auth_header = self.authorization_header(key_id, key)?;
        let header = HeaderValue::from_str(&auth_header).or(Err(CreationError::NoHeaders))?;
        self.headers_mut().insert(AUTHORIZATION, header);

        Ok(self)
    }

    fn with_signature_header(
        &mut self,
        key_id: String,
        key: CreateKey,
    ) -> Result<&mut Self, Error> {
        let sig_header = self.signature_header(key_id, key)?;
        let header = HeaderValue::from_str(&sig_header).or(Err(CreationError::NoHeaders))?;
        self.headers_mut().insert("Signature", header);

        Ok(self)
    }
}

#[cfg(test)]
mod tests {
    use std::{fs::File, io::Read};

    use reqwest::{
        header::{HeaderMap, CONTENT_LENGTH, CONTENT_TYPE, DATE, HOST},
        Client, Request,
    };
    use ring::signature::RSAKeyPair;
    use untrusted::Input;

    use crate::{
        create::{CreateKey, SigningString},
        prelude::*,
        ShaSize,
    };

    /* Request used for all tests:
     *
     * POST /foo HTTP/1.1
     * Host: example.org
     * Date: Tue, 07 Jun 2014 20:51:35 GMT
     * Content-Type: application/json
     * Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
     * Content-Length: 18
     *
     * {"hello": "world"}
     */

    const KEY_ID: &'static str = "rsa-key-1";
    const PRIVATE_KEY_PATH: &'static str = "tests/assets/private.der";

    #[test]
    fn min_test() {
        let uri = "http://example.org/foo";
        let req = Client::new().post(uri).build().unwrap();

        test_request(req, "(request-target): post /foo");
    }

    #[test]
    fn full_test() {
        let uri = "http://example.org/foo";

        let mut headers = HeaderMap::new();

        headers.insert(HOST, "example.org".parse().unwrap());
        headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
        headers.insert(
            "Digest",
            "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="
                .parse()
                .unwrap(),
        );
        headers.insert(DATE, "Tue, 07 Jun 2014 20:51:35 GMT".parse().unwrap());
        headers.insert(CONTENT_LENGTH, "18".parse().unwrap());

        let req = Client::new()
            .post(uri)
            .headers(headers)
            .body(r#"{"hello": "world"}"#)
            .build()
            .unwrap();

        test_request(
            req,
            "(request-target): post /foo
content-length: 18
content-type: application/json
date: Tue, 07 Jun 2014 20:51:35 GMT
digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
host: example.org",
        )
    }

    fn test_request(req: Request, s: &str) {
        let mut key = File::open(PRIVATE_KEY_PATH).unwrap();
        let mut key_vec = Vec::new();
        key.read_to_end(&mut key_vec).unwrap();
        let key_input = Input::from(&key_vec);
        let key = RSAKeyPair::from_der(key_input).unwrap();

        let http_sig = req
            .as_http_signature(KEY_ID.into(), CreateKey::rsa(key, ShaSize::SHA256))
            .unwrap();

        let signing_string: SigningString = http_sig.into();

        assert_eq!(signing_string.signing_string, s);
    }
}