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_hyper` feature. This modulde defines the `AsHttpSignature` and
//! `WithHttpSignature` traits for `hyper::Request`.
//!
//! This is useful for interacting with HTTP Signatures from Hyper-based applications, since it can
//! automatically generate signatures and add them to requests.
//!
//! # Example generating a signature
//! This example shows getting an `HttpSignature` type from a `hyper::Request`. Typically you
//! wouldn't want to do this directly, you'd use `with_authorization_header` or
//! `with_signature_header` directly, but in the event you want the intermediate state, this is
//! available.
//!
//! ```rust
//! # extern crate hyper;
//! # extern crate http_signatures;
//! # extern crate ring;
//! #
//! # use std::{error::Error, fs::File, io::Read};
//! #
//! # use http_signatures::{prelude::*, CreateKey, Input, ShaSize, SignatureAlgorithm};
//! # use hyper::{Method, Request};
//! # 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 uri = "https://example.com";
//!
//! let req = Request::post(uri)
//!     .body(()).unwrap();
//!
//! let http_sig = req.as_http_signature("rsa-key-1".into(), creation_key)?;
//! #     Ok(())
//! # }
//! # fn main() {
//! #     run().unwrap();
//! # }
//! ```
//!
//! # Example adding a signature to a Request type
//!
//! 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 hyper;
//! # extern crate http_signatures;
//! # extern crate ring;
//! #
//! # use std::{error::Error, fs::File, io::Read};
//! #
//! # use http_signatures::{prelude::*, CreateKey, Input, ShaSize, SignatureAlgorithm};
//! # use hyper::{Method, Request};
//! # 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 uri = "https://example.com";
//!
//! let mut req: Request<_> = Request::post(uri)
//!     .body(()).unwrap();
//!
//! 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/hyper-client)
//! for more information.

use std::collections::BTreeMap;

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

use hyper::{header::HeaderValue, Request as HyperRequest};

/// An implementation of `AsHttpSignature` for `hyper::Request`.
///
/// This trait is not often used directly, but is required by the `WithHttpSignature` trait defined
/// below.
impl<B> AsHttpSignature for HyperRequest<B> {
    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.uri().query() {
                format!(
                    "{} {}?{}",
                    self.method().as_ref().to_lowercase(),
                    self.uri().path(),
                    query
                )
            } else {
                format!(
                    "{} {}",
                    self.method().as_ref().to_lowercase(),
                    self.uri().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)
    }
}

/// An implementation of `WithHttpSignature` for `hyper::Request`
///
/// This automatically adds an Authorization header to a given `hyper::Request` struct containing
/// an HTTP Signature.
///
/// See
/// [this example](https://git.asonix.dog/asonix/http-signatures/src/branch/master/examples/hyper-client)
/// for usage information.
impl<B> WithHttpSignature for HyperRequest<B> {
    fn with_authorization_header(
        &mut self,
        key_id: String,
        key: CreateKey,
    ) -> Result<&mut Self, Error> {
        use hyper::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 hyper::{
        header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, DATE, HOST},
        Request, Uri,
    };
    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: Uri = "http://example.org/foo".parse().unwrap();
        let req = Request::post(uri).body(()).unwrap();

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

    #[test]
    fn full_test() {
        let uri: Uri = "http://example.org/foo".parse().unwrap();
        let body = r#"{"hello": "world"}"#;
        let mut req = Request::post(uri).body(body).unwrap();

        req.headers_mut()
            .insert(HOST, HeaderValue::from_str("example.org").unwrap());
        req.headers_mut().insert(
            CONTENT_TYPE,
            HeaderValue::from_str("application/json").unwrap(),
        );
        req.headers_mut().insert(
            "Digest",
            HeaderValue::from_str("SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=").unwrap(),
        );
        req.headers_mut().insert(
            DATE,
            HeaderValue::from_str("Tue, 07 Jun 2014 20:51:35 GMT").unwrap(),
        );
        req.headers_mut()
            .insert(CONTENT_LENGTH, HeaderValue::from_str("18").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<B>(req: Request<B>, 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).into())
            .unwrap();

        let signing_string: SigningString = http_sig.into();

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