hawk 5.0.1

Hawk Implementation for Rust
Documentation
use crate::credentials::Key;
use crate::error::*;
use crate::header::Header;
use crate::mac::{Mac, MacType};

/// A Response represents a response from an HTTP server.
///
/// The structure is created from a request and then used to either create (server) or validate
/// (client) a `Server-Authentication` header.
///
/// Like `Request`, Responses are built with `ResponseBuilders`.
///
/// # Examples
///
/// See the documentation in the crate root for examples.
#[derive(Debug, Clone)]
pub struct Response<'a> {
    method: &'a str,
    host: &'a str,
    port: u16,
    path: &'a str,
    req_header: &'a Header,
    hash: Option<&'a [u8]>,
    ext: Option<&'a str>,
}

impl<'a> Response<'a> {
    /// Create a new Header for this response, based on the given request and request header
    pub fn make_header(&self, key: &Key) -> Result<Header> {
        let ts = self.req_header.ts.ok_or(Error::MissingTs)?;
        let nonce = self.req_header.nonce.as_ref().ok_or(Error::MissingNonce)?;
        let mac = Mac::new(
            MacType::Response,
            key,
            ts,
            nonce,
            self.method,
            self.host,
            self.port,
            self.path,
            self.hash,
            self.ext,
        )?;

        // Per JS implementation, the Server-Authorization header includes only mac, hash, and ext
        Header::new(
            None,
            None,
            None,
            Some(mac),
            self.ext.map(|v| v.to_string()),
            self.hash.map(|v| v.to_vec()),
            None,
            None,
        )
    }

    /// Validate a Server-Authorization header.
    ///
    /// This checks that the MAC matches and, if a hash has been supplied locally,
    /// checks that one was provided from the server and that it, too, matches.
    pub fn validate_header(&self, response_header: &Header, key: &Key) -> bool {
        // extract required fields, returning early if they are not present
        let ts = match self.req_header.ts {
            Some(ts) => ts,
            None => {
                return false;
            }
        };
        let nonce = match self.req_header.nonce {
            Some(ref nonce) => nonce,
            None => {
                return false;
            }
        };
        let header_mac = match response_header.mac {
            Some(ref mac) => mac,
            None => {
                return false;
            }
        };
        let header_ext = response_header.ext.as_ref().map(|ext| &ext[..]);
        let header_hash = response_header.hash.as_ref().map(|hash| &hash[..]);

        // first verify the MAC
        match Mac::new(
            MacType::Response,
            key,
            ts,
            nonce,
            self.method,
            self.host,
            self.port,
            self.path,
            header_hash,
            header_ext,
        ) {
            Ok(calculated_mac) => {
                if &calculated_mac != header_mac {
                    return false;
                }
            }
            Err(_) => {
                return false;
            }
        };

        // ..then the hashes
        if let Some(local_hash) = self.hash {
            if let Some(server_hash) = header_hash {
                if local_hash != server_hash {
                    return false;
                }
            } else {
                return false;
            }
        }

        // NOTE: the timestamp self.req_header.ts was generated locally, so
        // there is no need to verify it

        true
    }
}

#[derive(Debug, Clone)]
pub struct ResponseBuilder<'a>(Response<'a>);

impl<'a> ResponseBuilder<'a> {
    /// Generate a new Response from a request header.
    ///
    /// This is more commonly accessed through `Request::make_response`.
    pub fn from_request_header(
        req_header: &'a Header,
        method: &'a str,
        host: &'a str,
        port: u16,
        path: &'a str,
    ) -> Self {
        ResponseBuilder(Response {
            method,
            host,
            port,
            path,
            req_header,
            hash: None,
            ext: None,
        })
    }

    /// Set the content hash for the response.
    ///
    /// This should always be calculated from the response payload, not copied from a header.
    pub fn hash<H: Into<Option<&'a [u8]>>>(mut self, hash: H) -> Self {
        self.0.hash = hash.into();
        self
    }

    /// Set the `ext` Hawk property for the response.
    ///
    /// This need only be set on the server; it is ignored in validating responses on the client.
    pub fn ext<S: Into<Option<&'a str>>>(mut self, ext: S) -> Self {
        self.0.ext = ext.into();
        self
    }

    /// Get the response from this builder
    pub fn response(self) -> Response<'a> {
        self.0
    }
}

#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
mod test {
    use super::ResponseBuilder;
    use crate::credentials::Key;
    use crate::header::Header;
    use crate::mac::Mac;
    use std::time::{Duration, UNIX_EPOCH};

    fn make_req_header() -> Header {
        Header::new(
            None,
            Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
            Some("j4h3g2"),
            None,
            None,
            None,
            None,
            None,
        )
        .unwrap()
    }

    #[test]
    fn test_validation_no_hash() {
        let req_header = make_req_header();
        let resp =
            ResponseBuilder::from_request_header(&req_header, "POST", "localhost", 9988, "/a/b")
                .response();
        let mac: Mac = Mac::from(vec![
            48, 133, 228, 163, 224, 197, 222, 77, 117, 81, 143, 73, 71, 120, 68, 238, 228, 40, 55,
            64, 190, 73, 102, 123, 79, 185, 199, 26, 62, 1, 137, 170,
        ]);
        let server_header = Header::new(
            None,
            None,
            None,
            Some(mac),
            Some("server-ext"),
            None,
            None,
            None,
        )
        .unwrap();
        assert!(resp.validate_header(&server_header, &Key::new("tok", crate::SHA256).unwrap()));
    }

    #[test]
    fn test_validation_hash_in_header() {
        // When a hash is provided in the response header, but no hash is added to the Response,
        // it is ignored (so validation succeeds)
        let req_header = make_req_header();
        let resp =
            ResponseBuilder::from_request_header(&req_header, "POST", "localhost", 9988, "/a/b")
                .response();
        let mac: Mac = Mac::from(vec![
            33, 147, 159, 211, 184, 194, 189, 74, 53, 229, 241, 161, 215, 145, 22, 34, 206, 207,
            242, 100, 33, 193, 36, 96, 149, 133, 180, 4, 132, 87, 207, 238,
        ]);
        let server_header = Header::new(
            None,
            None,
            None,
            Some(mac),
            Some("server-ext"),
            Some(vec![1, 2, 3, 4]),
            None,
            None,
        )
        .unwrap();
        assert!(resp.validate_header(&server_header, &Key::new("tok", crate::SHA256).unwrap()));
    }

    #[test]
    fn test_validation_hash_required_but_not_given() {
        // When Response.hash is called, but no hash is in the hader, validation fails.
        let req_header = make_req_header();
        let hash = vec![1, 2, 3, 4];
        let resp =
            ResponseBuilder::from_request_header(&req_header, "POST", "localhost", 9988, "/a/b")
                .hash(&hash[..])
                .response();
        let mac: Mac = Mac::from(vec![
            48, 133, 228, 163, 224, 197, 222, 77, 117, 81, 143, 73, 71, 120, 68, 238, 228, 40, 55,
            64, 190, 73, 102, 123, 79, 185, 199, 26, 62, 1, 137, 170,
        ]);
        let server_header = Header::new(
            None,
            None,
            None,
            Some(mac),
            Some("server-ext"),
            None,
            None,
            None,
        )
        .unwrap();
        assert!(!resp.validate_header(&server_header, &Key::new("tok", crate::SHA256).unwrap()));
    }

    #[test]
    fn test_validation_hash_validated() {
        // When a hash is provided in the response header and the Response.hash method is called,
        // the two must match
        let req_header = make_req_header();
        let hash = vec![1, 2, 3, 4];
        let resp =
            ResponseBuilder::from_request_header(&req_header, "POST", "localhost", 9988, "/a/b")
                .hash(&hash[..])
                .response();
        let mac: Mac = Mac::from(vec![
            33, 147, 159, 211, 184, 194, 189, 74, 53, 229, 241, 161, 215, 145, 22, 34, 206, 207,
            242, 100, 33, 193, 36, 96, 149, 133, 180, 4, 132, 87, 207, 238,
        ]);
        let server_header = Header::new(
            None,
            None,
            None,
            Some(mac),
            Some("server-ext"),
            Some(vec![1, 2, 3, 4]),
            None,
            None,
        )
        .unwrap();
        assert!(resp.validate_header(&server_header, &Key::new("tok", crate::SHA256).unwrap()));

        // a different supplied hash won't match..
        let hash = vec![99, 99, 99, 99];
        let resp =
            ResponseBuilder::from_request_header(&req_header, "POST", "localhost", 9988, "/a/b")
                .hash(&hash[..])
                .response();
        assert!(!resp.validate_header(&server_header, &Key::new("tok", crate::SHA256).unwrap()));
    }
}