Revision control

Copy as Markdown

Other Tools

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()));
}
}