summaryrefslogtreecommitdiffstats
path: root/third_party/rust/hawk/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/hawk/src
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/hawk/src')
-rw-r--r--third_party/rust/hawk/src/bewit.rs212
-rw-r--r--third_party/rust/hawk/src/credentials.rs61
-rw-r--r--third_party/rust/hawk/src/crypto/holder.rs52
-rw-r--r--third_party/rust/hawk/src/crypto/mod.rs83
-rw-r--r--third_party/rust/hawk/src/crypto/openssl.rs98
-rw-r--r--third_party/rust/hawk/src/crypto/ring.rs99
-rw-r--r--third_party/rust/hawk/src/error.rs70
-rw-r--r--third_party/rust/hawk/src/header.rs498
-rw-r--r--third_party/rust/hawk/src/lib.rs173
-rw-r--r--third_party/rust/hawk/src/mac.rs200
-rw-r--r--third_party/rust/hawk/src/payload.rs87
-rw-r--r--third_party/rust/hawk/src/request.rs974
-rw-r--r--third_party/rust/hawk/src/response.rs320
13 files changed, 2927 insertions, 0 deletions
diff --git a/third_party/rust/hawk/src/bewit.rs b/third_party/rust/hawk/src/bewit.rs
new file mode 100644
index 0000000000..ea3db202a3
--- /dev/null
+++ b/third_party/rust/hawk/src/bewit.rs
@@ -0,0 +1,212 @@
+use crate::error::*;
+use crate::mac::Mac;
+use std::borrow::Cow;
+use std::str;
+use std::str::FromStr;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+/// A Bewit is a piece of data attached to a GET request that functions in place of a Hawk
+/// Authentication header. It contains an id, a timestamp, a MAC, and an optional `ext` value.
+/// These are available using accessor functions.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Bewit<'a> {
+ id: Cow<'a, str>,
+ exp: SystemTime,
+ mac: Cow<'a, Mac>,
+ ext: Option<Cow<'a, str>>,
+}
+
+impl<'a> Bewit<'a> {
+ /// Create a new Bewit with the given values.
+ ///
+ /// See Request.make_bewit for an easier way to make a Bewit
+ pub fn new(id: &'a str, exp: SystemTime, mac: Mac, ext: Option<&'a str>) -> Bewit<'a> {
+ Bewit {
+ id: Cow::Borrowed(id),
+ exp,
+ mac: Cow::Owned(mac),
+ ext: match ext {
+ Some(s) => Some(Cow::Borrowed(s)),
+ None => None,
+ },
+ }
+ }
+
+ /// Generate the fully-encoded string for this Bewit
+ pub fn to_str(&self) -> String {
+ use base64::display::Base64Display;
+ let raw = format!(
+ "{}\\{}\\{}\\{}",
+ self.id,
+ self.exp
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_secs(),
+ Base64Display::with_config(self.mac.as_ref(), base64::STANDARD),
+ match self.ext {
+ Some(ref cow) => cow.as_ref(),
+ None => "",
+ }
+ );
+
+ base64::encode_config(&raw, base64::URL_SAFE_NO_PAD)
+ }
+
+ /// Get the Bewit's client identifier
+ pub fn id(&self) -> &str {
+ self.id.as_ref()
+ }
+
+ /// Get the expiration time of the bewit
+ pub fn exp(&self) -> SystemTime {
+ self.exp
+ }
+
+ /// Get the MAC included in the Bewit
+ pub fn mac(&self) -> &Mac {
+ self.mac.as_ref()
+ }
+
+ /// Get the Bewit's `ext` field.
+ pub fn ext(&self) -> Option<&str> {
+ match self.ext {
+ Some(ref cow) => Some(cow.as_ref()),
+ None => None,
+ }
+ }
+}
+
+const BACKSLASH: u8 = b'\\';
+
+impl<'a> FromStr for Bewit<'a> {
+ type Err = Error;
+ fn from_str(bewit: &str) -> Result<Bewit<'a>> {
+ let bewit = base64::decode(bewit)?;
+
+ let parts: Vec<&[u8]> = bewit.split(|c| *c == BACKSLASH).collect();
+ if parts.len() != 4 {
+ return Err(InvalidBewit::Format.into());
+ }
+
+ let id = String::from_utf8(parts[0].to_vec()).map_err(|_| InvalidBewit::Id)?;
+
+ let exp = str::from_utf8(parts[1]).map_err(|_| InvalidBewit::Exp)?;
+ let exp = u64::from_str(exp).map_err(|_| InvalidBewit::Exp)?;
+ let exp = UNIX_EPOCH + Duration::new(exp, 0);
+
+ let mac = str::from_utf8(parts[2]).map_err(|_| InvalidBewit::Mac)?;
+ let mac = Mac::from(base64::decode(mac).map_err(|_| InvalidBewit::Mac)?);
+
+ let ext = match parts[3].len() {
+ 0 => None,
+ _ => Some(Cow::Owned(
+ String::from_utf8(parts[3].to_vec()).map_err(|_| InvalidBewit::Ext)?,
+ )),
+ };
+
+ Ok(Bewit {
+ id: Cow::Owned(id),
+ exp,
+ mac: Cow::Owned(mac),
+ ext,
+ })
+ }
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod test {
+ use super::*;
+ use crate::credentials::Key;
+ use crate::mac::{Mac, MacType};
+ use std::str::FromStr;
+
+ const BEWIT_STR: &str =
+ "bWVcMTM1MzgzMjgzNFxmaXk0ZTV3QmRhcEROeEhIZUExOE5yU3JVMVUzaVM2NmdtMFhqVEpwWXlVPVw";
+ const BEWIT_WITH_EXT_STR: &str =
+ "bWVcMTM1MzgzMjgzNFxmaXk0ZTV3QmRhcEROeEhIZUExOE5yU3JVMVUzaVM2NmdtMFhqVEpwWXlVPVxhYmNk";
+
+ fn make_mac() -> Mac {
+ let key = Key::new(
+ vec![
+ 11u8, 19, 228, 209, 79, 189, 200, 59, 166, 47, 86, 254, 235, 184, 120, 197, 75,
+ 152, 201, 79, 115, 61, 111, 242, 219, 187, 173, 14, 227, 108, 60, 232,
+ ],
+ crate::DigestAlgorithm::Sha256,
+ )
+ .unwrap();
+ Mac::new(
+ MacType::Header,
+ &key,
+ UNIX_EPOCH + Duration::new(1353832834, 100),
+ "nonny",
+ "POST",
+ "mysite.com",
+ 443,
+ "/v1/api",
+ None,
+ None,
+ )
+ .unwrap()
+ }
+
+ #[test]
+ fn test_to_str() {
+ let bewit = Bewit::new(
+ "me",
+ UNIX_EPOCH + Duration::new(1353832834, 0),
+ make_mac(),
+ None,
+ );
+ assert_eq!(bewit.to_str(), BEWIT_STR);
+ let bewit = Bewit::new(
+ "me",
+ UNIX_EPOCH + Duration::new(1353832834, 0),
+ make_mac(),
+ Some("abcd"),
+ );
+ assert_eq!(bewit.to_str(), BEWIT_WITH_EXT_STR);
+ }
+
+ #[test]
+ fn test_accessors() {
+ let bewit = Bewit::from_str(BEWIT_STR).unwrap();
+ assert_eq!(bewit.id(), "me");
+ assert_eq!(bewit.exp(), UNIX_EPOCH + Duration::new(1353832834, 0));
+ assert_eq!(bewit.mac(), &make_mac());
+ assert_eq!(bewit.ext(), None);
+ }
+
+ #[test]
+ fn test_from_str_invalid_base64() {
+ assert!(Bewit::from_str("!/==").is_err());
+ }
+
+ #[test]
+ fn test_from_str_invalid_too_many_parts() {
+ let bewit = base64::encode(&"a\\123\\abc\\ext\\WHUT?".as_bytes());
+ assert!(Bewit::from_str(&bewit).is_err());
+ }
+
+ #[test]
+ fn test_from_str_invalid_too_few_parts() {
+ let bewit = base64::encode(&"a\\123\\abc".as_bytes());
+ assert!(Bewit::from_str(&bewit).is_err());
+ }
+
+ #[test]
+ fn test_from_str_invalid_not_utf8() {
+ let a = 'a' as u8;
+ let one = '1' as u8;
+ let slash = '\\' as u8;
+ let invalid1 = 0u8;
+ let invalid2 = 159u8;
+ let bewit = base64::encode(&[invalid1, invalid2, slash, one, slash, a, slash, a]);
+ assert!(Bewit::from_str(&bewit).is_err());
+ let bewit = base64::encode(&[a, slash, invalid1, invalid2, slash, a, slash, a]);
+ assert!(Bewit::from_str(&bewit).is_err());
+ let bewit = base64::encode(&[a, slash, one, slash, invalid1, invalid2, slash, a]);
+ assert!(Bewit::from_str(&bewit).is_err());
+ let bewit = base64::encode(&[a, slash, one, slash, a, slash, invalid1, invalid2]);
+ assert!(Bewit::from_str(&bewit).is_err());
+ }
+}
diff --git a/third_party/rust/hawk/src/credentials.rs b/third_party/rust/hawk/src/credentials.rs
new file mode 100644
index 0000000000..ec2c5025a4
--- /dev/null
+++ b/third_party/rust/hawk/src/credentials.rs
@@ -0,0 +1,61 @@
+use crate::crypto::{self, HmacKey};
+
+#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
+pub enum DigestAlgorithm {
+ Sha256,
+ Sha384,
+ Sha512,
+ // Indicate that this isn't an enum that anyone should match on, and that we
+ // reserve the right to add to this enumeration without making a major
+ // version bump. Once https://github.com/rust-lang/rfcs/blob/master/text/2008-non-exhaustive.md
+ // is stabilized, that should be used instead.
+ #[doc(hidden)]
+ _Nonexhaustive,
+}
+
+/// Hawk key.
+///
+/// While any sequence of bytes can be specified as a key, note that each digest algorithm has
+/// a suggested key length, and that passwords should *not* be used as keys. Keys of incorrect
+/// length are handled according to the digest's implementation.
+pub struct Key(Box<dyn HmacKey>);
+
+impl Key {
+ pub fn new<B>(key: B, algorithm: DigestAlgorithm) -> crate::Result<Key>
+ where
+ B: AsRef<[u8]>,
+ {
+ Ok(Key(crypto::new_key(algorithm, key.as_ref())?))
+ }
+
+ pub fn sign(&self, data: &[u8]) -> crate::Result<Vec<u8>> {
+ Ok(self.0.sign(data)?)
+ }
+}
+
+/// Hawk credentials: an ID and a key associated with that ID. The digest algorithm
+/// must be agreed between the server and the client, and the length of the key is
+/// specific to that algorithm.
+pub struct Credentials {
+ pub id: String,
+ pub key: Key,
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_new_sha256() {
+ let key = vec![77u8; 32];
+ // hmac::SigningKey doesn't allow any visibilty inside, so we just build the
+ // key and assume it works..
+ Key::new(key, DigestAlgorithm::Sha256).unwrap();
+ }
+
+ #[test]
+ fn test_new_sha256_bad_length() {
+ let key = vec![0u8; 99];
+ Key::new(key, DigestAlgorithm::Sha256).unwrap();
+ }
+}
diff --git a/third_party/rust/hawk/src/crypto/holder.rs b/third_party/rust/hawk/src/crypto/holder.rs
new file mode 100644
index 0000000000..c99332d745
--- /dev/null
+++ b/third_party/rust/hawk/src/crypto/holder.rs
@@ -0,0 +1,52 @@
+use super::Cryptographer;
+use once_cell::sync::OnceCell;
+
+static CRYPTOGRAPHER: OnceCell<&'static dyn Cryptographer> = OnceCell::new();
+
+#[derive(Debug, thiserror::Error)]
+#[error("Cryptographer already initialized")]
+pub struct SetCryptographerError(());
+
+/// Sets the global object that will be used for cryptographic operations.
+///
+/// This is a convenience wrapper over [`set_cryptographer`],
+/// but takes a `Box<dyn Cryptographer>` instead.
+#[cfg(not(any(feature = "use_ring", feature = "use_openssl")))]
+pub fn set_boxed_cryptographer(c: Box<dyn Cryptographer>) -> Result<(), SetCryptographerError> {
+ // Just leak the Box. It wouldn't be freed as a `static` anyway, and we
+ // never allow this to be re-assigned (so it's not a meaningful memory leak).
+ set_cryptographer(Box::leak(c))
+}
+
+/// Sets the global object that will be used for cryptographic operations.
+///
+/// This function may only be called once in the lifetime of a program.
+///
+/// Any calls into this crate that perform cryptography prior to calling this
+/// function will panic.
+pub fn set_cryptographer(c: &'static dyn Cryptographer) -> Result<(), SetCryptographerError> {
+ CRYPTOGRAPHER.set(c).map_err(|_| SetCryptographerError(()))
+}
+
+pub(crate) fn get_crypographer() -> &'static dyn Cryptographer {
+ autoinit_crypto();
+ *CRYPTOGRAPHER
+ .get()
+ .expect("`hawk` cryptographer not initialized!")
+}
+
+#[cfg(feature = "use_ring")]
+#[inline]
+fn autoinit_crypto() {
+ let _ = set_cryptographer(&super::ring::RingCryptographer);
+}
+
+#[cfg(feature = "use_openssl")]
+#[inline]
+fn autoinit_crypto() {
+ let _ = set_cryptographer(&super::openssl::OpensslCryptographer);
+}
+
+#[cfg(not(any(feature = "use_openssl", feature = "use_ring")))]
+#[inline]
+fn autoinit_crypto() {}
diff --git a/third_party/rust/hawk/src/crypto/mod.rs b/third_party/rust/hawk/src/crypto/mod.rs
new file mode 100644
index 0000000000..ef2243a48d
--- /dev/null
+++ b/third_party/rust/hawk/src/crypto/mod.rs
@@ -0,0 +1,83 @@
+//! `hawk` must perform certain cryptographic operations in order to function,
+//! and applications may need control over which library is used for these.
+//!
+//! This module can be used for that purpose. If you do not care, this crate can
+//! be configured so that a default implementation is provided based on either
+//! `ring` or `openssl` (via the `use_ring` and `use_openssl` features respectively).
+//!
+//! Should you need something custom, then you can provide it by implementing
+//! [`Cryptographer`] and using the [`set_cryptographer`] or
+//! [`set_boxed_cryptographer`] functions.
+use crate::DigestAlgorithm;
+
+pub(crate) mod holder;
+pub(crate) use holder::get_crypographer;
+
+#[cfg(feature = "use_openssl")]
+mod openssl;
+#[cfg(feature = "use_ring")]
+mod ring;
+
+#[cfg(not(any(feature = "use_ring", feature = "use_openssl")))]
+pub use self::holder::{set_boxed_cryptographer, set_cryptographer};
+
+#[derive(Debug, thiserror::Error)]
+pub enum CryptoError {
+ /// The configured cryptographer does not support the digest algorithm
+ /// specified. This should only happen for custom `Cryptographer` implementations
+ #[error("Digest algorithm {0:?} is unsupported by this Cryptographer")]
+ UnsupportedDigest(DigestAlgorithm),
+
+ /// The configured cryptographer implementation failed to perform an
+ /// operation in some way.
+ #[error("{0}")]
+ Other(#[source] anyhow::Error),
+}
+
+/// A trait encapsulating the cryptographic operations required by this library.
+///
+/// If you use this library with either the `use_ring` or `use_openssl` features enabled,
+/// then you do not have to worry about this.
+pub trait Cryptographer: Send + Sync + 'static {
+ fn rand_bytes(&self, output: &mut [u8]) -> Result<(), CryptoError>;
+ fn new_key(
+ &self,
+ algorithm: DigestAlgorithm,
+ key: &[u8],
+ ) -> Result<Box<dyn HmacKey>, CryptoError>;
+ fn new_hasher(&self, algo: DigestAlgorithm) -> Result<Box<dyn Hasher>, CryptoError>;
+ fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool;
+}
+
+/// Type-erased hmac key type.
+pub trait HmacKey: Send + Sync + 'static {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CryptoError>;
+}
+
+/// Type-erased hash context type.
+pub trait Hasher: Send + Sync + 'static {
+ fn update(&mut self, data: &[u8]) -> Result<(), CryptoError>;
+ // Note: this would take by move but that's not object safe :(
+ fn finish(&mut self) -> Result<Vec<u8>, CryptoError>;
+}
+
+// For convenience
+
+pub(crate) fn rand_bytes(buffer: &mut [u8]) -> Result<(), CryptoError> {
+ get_crypographer().rand_bytes(buffer)
+}
+
+pub(crate) fn new_key(
+ algorithm: DigestAlgorithm,
+ key: &[u8],
+) -> Result<Box<dyn HmacKey>, CryptoError> {
+ get_crypographer().new_key(algorithm, key)
+}
+
+pub(crate) fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
+ get_crypographer().constant_time_compare(a, b)
+}
+
+pub(crate) fn new_hasher(algorithm: DigestAlgorithm) -> Result<Box<dyn Hasher>, CryptoError> {
+ Ok(get_crypographer().new_hasher(algorithm)?)
+}
diff --git a/third_party/rust/hawk/src/crypto/openssl.rs b/third_party/rust/hawk/src/crypto/openssl.rs
new file mode 100644
index 0000000000..b925ee9f5e
--- /dev/null
+++ b/third_party/rust/hawk/src/crypto/openssl.rs
@@ -0,0 +1,98 @@
+use super::{CryptoError, Cryptographer, Hasher, HmacKey};
+use crate::DigestAlgorithm;
+use std::convert::{TryFrom, TryInto};
+
+use openssl::error::ErrorStack;
+use openssl::hash::MessageDigest;
+use openssl::pkey::{PKey, Private};
+use openssl::sign::Signer;
+
+impl From<ErrorStack> for CryptoError {
+ fn from(e: ErrorStack) -> Self {
+ CryptoError::Other(e.into())
+ }
+}
+
+pub struct OpensslCryptographer;
+
+struct OpensslHmacKey {
+ key: PKey<Private>,
+ digest: MessageDigest,
+}
+
+impl HmacKey for OpensslHmacKey {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ let mut hmac_signer = Signer::new(self.digest, &self.key)?;
+ hmac_signer.update(&data)?;
+ let digest = hmac_signer.sign_to_vec()?;
+ let mut mac = vec![0; self.digest.size()];
+ mac.clone_from_slice(digest.as_ref());
+ Ok(mac)
+ }
+}
+
+// This is always `Some` until `finish` is called.
+struct OpensslHasher(Option<openssl::hash::Hasher>);
+
+impl Hasher for OpensslHasher {
+ fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> {
+ self.0
+ .as_mut()
+ .expect("update called after `finish`")
+ .update(data)?;
+ Ok(())
+ }
+
+ fn finish(&mut self) -> Result<Vec<u8>, CryptoError> {
+ let digest = self.0.take().expect("`finish` called twice").finish()?;
+ let bytes: &[u8] = digest.as_ref();
+ Ok(bytes.to_owned())
+ }
+}
+
+impl Cryptographer for OpensslCryptographer {
+ fn rand_bytes(&self, output: &mut [u8]) -> Result<(), CryptoError> {
+ openssl::rand::rand_bytes(output)?;
+ Ok(())
+ }
+
+ fn new_key(
+ &self,
+ algorithm: DigestAlgorithm,
+ key: &[u8],
+ ) -> Result<Box<dyn HmacKey>, CryptoError> {
+ let digest = algorithm.try_into()?;
+ Ok(Box::new(OpensslHmacKey {
+ key: PKey::hmac(key)?,
+ digest,
+ }))
+ }
+
+ fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool {
+ // openssl::memcmp::eq panics if the lengths are not the same. ring
+ // returns `Err` (and notes in the docs that it is not constant time if
+ // the lengths are not the same). We make this behave like ring.
+ if a.len() != b.len() {
+ false
+ } else {
+ openssl::memcmp::eq(a, b)
+ }
+ }
+
+ fn new_hasher(&self, algorithm: DigestAlgorithm) -> Result<Box<dyn Hasher>, CryptoError> {
+ let ctx = openssl::hash::Hasher::new(algorithm.try_into()?)?;
+ Ok(Box::new(OpensslHasher(Some(ctx))))
+ }
+}
+
+impl TryFrom<DigestAlgorithm> for MessageDigest {
+ type Error = CryptoError;
+ fn try_from(algorithm: DigestAlgorithm) -> Result<Self, CryptoError> {
+ match algorithm {
+ DigestAlgorithm::Sha256 => Ok(MessageDigest::sha256()),
+ DigestAlgorithm::Sha384 => Ok(MessageDigest::sha384()),
+ DigestAlgorithm::Sha512 => Ok(MessageDigest::sha512()),
+ algo => Err(CryptoError::UnsupportedDigest(algo)),
+ }
+ }
+}
diff --git a/third_party/rust/hawk/src/crypto/ring.rs b/third_party/rust/hawk/src/crypto/ring.rs
new file mode 100644
index 0000000000..6721b9352c
--- /dev/null
+++ b/third_party/rust/hawk/src/crypto/ring.rs
@@ -0,0 +1,99 @@
+use super::{CryptoError, Cryptographer, Hasher, HmacKey};
+use crate::DigestAlgorithm;
+use ring::{digest, hmac};
+use std::convert::{TryFrom, TryInto};
+
+impl From<ring::error::Unspecified> for CryptoError {
+ // Ring's errors are entirely opaque
+ fn from(_: ring::error::Unspecified) -> Self {
+ CryptoError::Other(anyhow::Error::msg("Unspecified ring error"))
+ }
+}
+
+impl From<std::convert::Infallible> for CryptoError {
+ fn from(_: std::convert::Infallible) -> Self {
+ unreachable!()
+ }
+}
+
+pub struct RingCryptographer;
+
+struct RingHmacKey(hmac::Key);
+
+impl HmacKey for RingHmacKey {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
+ let digest = hmac::sign(&self.0, data);
+ let mut mac = vec![0; self.0.algorithm().digest_algorithm().output_len];
+ mac.copy_from_slice(digest.as_ref());
+ Ok(mac)
+ }
+}
+// This is always `Some` until `finish` is called.
+struct RingHasher(Option<digest::Context>);
+
+impl Hasher for RingHasher {
+ fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> {
+ self.0
+ .as_mut()
+ .expect("update called after `finish`")
+ .update(data);
+ Ok(())
+ }
+
+ fn finish(&mut self) -> Result<Vec<u8>, CryptoError> {
+ let digest = self.0.take().expect("`finish` called twice").finish();
+ let bytes: &[u8] = digest.as_ref();
+ Ok(bytes.to_owned())
+ }
+}
+
+impl Cryptographer for RingCryptographer {
+ fn rand_bytes(&self, output: &mut [u8]) -> Result<(), CryptoError> {
+ use ring::rand::SecureRandom;
+ let rnd = ring::rand::SystemRandom::new();
+ rnd.fill(output)?;
+ Ok(())
+ }
+
+ fn new_key(
+ &self,
+ algorithm: DigestAlgorithm,
+ key: &[u8],
+ ) -> Result<Box<dyn HmacKey>, CryptoError> {
+ let k = hmac::Key::new(algorithm.try_into()?, key);
+ Ok(Box::new(RingHmacKey(k)))
+ }
+
+ fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool {
+ ring::constant_time::verify_slices_are_equal(a, b).is_ok()
+ }
+
+ fn new_hasher(&self, algorithm: DigestAlgorithm) -> Result<Box<dyn Hasher>, CryptoError> {
+ let ctx = digest::Context::new(algorithm.try_into()?);
+ Ok(Box::new(RingHasher(Some(ctx))))
+ }
+}
+
+impl TryFrom<DigestAlgorithm> for &'static digest::Algorithm {
+ type Error = CryptoError;
+ fn try_from(algorithm: DigestAlgorithm) -> Result<Self, CryptoError> {
+ match algorithm {
+ DigestAlgorithm::Sha256 => Ok(&digest::SHA256),
+ DigestAlgorithm::Sha384 => Ok(&digest::SHA384),
+ DigestAlgorithm::Sha512 => Ok(&digest::SHA512),
+ algo => Err(CryptoError::UnsupportedDigest(algo)),
+ }
+ }
+}
+
+impl TryFrom<DigestAlgorithm> for hmac::Algorithm {
+ type Error = CryptoError;
+ fn try_from(algorithm: DigestAlgorithm) -> Result<Self, CryptoError> {
+ match algorithm {
+ DigestAlgorithm::Sha256 => Ok(hmac::HMAC_SHA256),
+ DigestAlgorithm::Sha384 => Ok(hmac::HMAC_SHA384),
+ DigestAlgorithm::Sha512 => Ok(hmac::HMAC_SHA512),
+ algo => Err(CryptoError::UnsupportedDigest(algo)),
+ }
+ }
+}
diff --git a/third_party/rust/hawk/src/error.rs b/third_party/rust/hawk/src/error.rs
new file mode 100644
index 0000000000..96884d430f
--- /dev/null
+++ b/third_party/rust/hawk/src/error.rs
@@ -0,0 +1,70 @@
+use crate::crypto::CryptoError;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Unparseable Hawk header: {0}")]
+ HeaderParseError(String),
+
+ #[error("Invalid url: {0}")]
+ InvalidUrl(String),
+
+ #[error("Missing `ts` attribute in Hawk header")]
+ MissingTs,
+
+ #[error("Missing `nonce` attribute in Hawk header")]
+ MissingNonce,
+
+ #[error("{0}")]
+ InvalidBewit(#[source] InvalidBewit),
+
+ #[error("{0}")]
+ Io(#[source] std::io::Error),
+
+ #[error("Base64 Decode error: {0}")]
+ Decode(#[source] base64::DecodeError),
+
+ #[error("Crypto error: {0}")]
+ Crypto(#[source] CryptoError),
+}
+
+#[derive(thiserror::Error, Debug, PartialEq)]
+pub enum InvalidBewit {
+ #[error("Multiple bewits in URL")]
+ Multiple,
+ #[error("Invalid bewit format")]
+ Format,
+ #[error("Invalid bewit id")]
+ Id,
+ #[error("Invalid bewit exp")]
+ Exp,
+ #[error("Invalid bewit mac")]
+ Mac,
+ #[error("Invalid bewit ext")]
+ Ext,
+}
+
+impl From<base64::DecodeError> for Error {
+ fn from(e: base64::DecodeError) -> Self {
+ Error::Decode(e)
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(e: std::io::Error) -> Self {
+ Error::Io(e)
+ }
+}
+
+impl From<CryptoError> for Error {
+ fn from(e: CryptoError) -> Self {
+ Error::Crypto(e)
+ }
+}
+
+impl From<InvalidBewit> for Error {
+ fn from(e: InvalidBewit) -> Self {
+ Error::InvalidBewit(e)
+ }
+}
diff --git a/third_party/rust/hawk/src/header.rs b/third_party/rust/hawk/src/header.rs
new file mode 100644
index 0000000000..910a202e83
--- /dev/null
+++ b/third_party/rust/hawk/src/header.rs
@@ -0,0 +1,498 @@
+use crate::error::*;
+use crate::mac::Mac;
+use base64::display::Base64Display;
+use std::fmt;
+use std::str::FromStr;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+/// Representation of a Hawk `Authorization` header value (the part following "Hawk ").
+///
+/// Headers can be derived from strings using the `FromStr` trait, and formatted into a
+/// string using the `fmt_header` method.
+///
+/// All fields are optional, although for specific purposes some fields must be present.
+#[derive(Clone, PartialEq, Debug)]
+pub struct Header {
+ pub id: Option<String>,
+ pub ts: Option<SystemTime>,
+ pub nonce: Option<String>,
+ pub mac: Option<Mac>,
+ pub ext: Option<String>,
+ pub hash: Option<Vec<u8>>,
+ pub app: Option<String>,
+ pub dlg: Option<String>,
+}
+
+impl Header {
+ /// Create a new Header with the full set of Hawk fields.
+ ///
+ /// This is a low-level function. Headers are more often created from Requests or Responses.
+ ///
+ /// Note that none of the string-formatted header components can contain the character `\"`.
+ pub fn new<S>(
+ id: Option<S>,
+ ts: Option<SystemTime>,
+ nonce: Option<S>,
+ mac: Option<Mac>,
+ ext: Option<S>,
+ hash: Option<Vec<u8>>,
+ app: Option<S>,
+ dlg: Option<S>,
+ ) -> Result<Header>
+ where
+ S: Into<String>,
+ {
+ Ok(Header {
+ id: Header::check_component(id)?,
+ ts,
+ nonce: Header::check_component(nonce)?,
+ mac,
+ ext: Header::check_component(ext)?,
+ hash,
+ app: Header::check_component(app)?,
+ dlg: Header::check_component(dlg)?,
+ })
+ }
+
+ /// Check a header component for validity.
+ fn check_component<S>(value: Option<S>) -> Result<Option<String>>
+ where
+ S: Into<String>,
+ {
+ if let Some(value) = value {
+ let value = value.into();
+ if value.contains('\"') {
+ return Err(Error::HeaderParseError(
+ "Hawk headers cannot contain `\\`".into(),
+ ));
+ }
+ Ok(Some(value))
+ } else {
+ Ok(None)
+ }
+ }
+
+ /// Format the header for transmission in an Authorization header, omitting the `"Hawk "`
+ /// prefix.
+ pub fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut sep = "";
+ if let Some(ref id) = self.id {
+ write!(f, "{}id=\"{}\"", sep, id)?;
+ sep = ", ";
+ }
+ if let Some(ref ts) = self.ts {
+ write!(
+ f,
+ "{}ts=\"{}\"",
+ sep,
+ ts.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
+ )?;
+ sep = ", ";
+ }
+ if let Some(ref nonce) = self.nonce {
+ write!(f, "{}nonce=\"{}\"", sep, nonce)?;
+ sep = ", ";
+ }
+ if let Some(ref mac) = self.mac {
+ write!(
+ f,
+ "{}mac=\"{}\"",
+ sep,
+ Base64Display::with_config(mac, base64::STANDARD)
+ )?;
+ sep = ", ";
+ }
+ if let Some(ref ext) = self.ext {
+ write!(f, "{}ext=\"{}\"", sep, ext)?;
+ sep = ", ";
+ }
+ if let Some(ref hash) = self.hash {
+ write!(
+ f,
+ "{}hash=\"{}\"",
+ sep,
+ Base64Display::with_config(hash, base64::STANDARD)
+ )?;
+ sep = ", ";
+ }
+ if let Some(ref app) = self.app {
+ write!(f, "{}app=\"{}\"", sep, app)?;
+ sep = ", ";
+ }
+ if let Some(ref dlg) = self.dlg {
+ write!(f, "{}dlg=\"{}\"", sep, dlg)?;
+ }
+ Ok(())
+ }
+}
+
+impl fmt::Display for Header {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.fmt_header(f)
+ }
+}
+
+impl FromStr for Header {
+ type Err = Error;
+ fn from_str(s: &str) -> Result<Header> {
+ let mut p = &s[..];
+
+ // Required attributes
+ let mut id: Option<&str> = None;
+ let mut ts: Option<SystemTime> = None;
+ let mut nonce: Option<&str> = None;
+ let mut mac: Option<Vec<u8>> = None;
+ // Optional attributes
+ let mut hash: Option<Vec<u8>> = None;
+ let mut ext: Option<&str> = None;
+ let mut app: Option<&str> = None;
+ let mut dlg: Option<&str> = None;
+
+ while !p.is_empty() {
+ // Skip whitespace and commas used as separators
+ p = p.trim_start_matches(|c| c == ',' || char::is_whitespace(c));
+ // Find first '=' which delimits attribute name from value
+ let assign_end = p
+ .find('=')
+ .ok_or_else(|| Error::HeaderParseError("Expected '='".into()))?;
+ let attr = &p[..assign_end].trim();
+ if p.len() < assign_end + 1 {
+ return Err(Error::HeaderParseError(
+ "Missing right hand side of =".into(),
+ ));
+ }
+ p = (&p[assign_end + 1..]).trim_start();
+ if !p.starts_with('\"') {
+ return Err(Error::HeaderParseError("Expected opening quote".into()));
+ }
+ p = &p[1..];
+ // We have poor RFC 7235 compliance here as we ought to support backslash
+ // escaped characters, but hawk doesn't allow this we won't either. All
+ // strings must be surrounded by ".." and contain no such characters.
+ let end = p.find('\"');
+ let val_end =
+ end.ok_or_else(|| Error::HeaderParseError("Expected closing quote".into()))?;
+ let val = &p[..val_end];
+ match *attr {
+ "id" => id = Some(val),
+ "ts" => {
+ let epoch = u64::from_str(val)
+ .map_err(|_| Error::HeaderParseError("Error parsing `ts` field".into()))?;
+ ts = Some(UNIX_EPOCH + Duration::new(epoch, 0));
+ }
+ "mac" => {
+ mac = Some(base64::decode(val).map_err(|_| {
+ Error::HeaderParseError("Error parsing `mac` field".into())
+ })?);
+ }
+ "nonce" => nonce = Some(val),
+ "ext" => ext = Some(val),
+ "hash" => {
+ hash = Some(base64::decode(val).map_err(|_| {
+ Error::HeaderParseError("Error parsing `hash` field".into())
+ })?);
+ }
+ "app" => app = Some(val),
+ "dlg" => dlg = Some(val),
+ _ => {
+ return Err(Error::HeaderParseError(format!(
+ "Invalid Hawk field {}",
+ *attr
+ )))
+ }
+ };
+ // Break if we are at end of string, otherwise skip separator
+ if p.len() < val_end + 1 {
+ break;
+ }
+ p = p[val_end + 1..].trim_start();
+ }
+
+ Ok(Header {
+ id: match id {
+ Some(id) => Some(id.to_string()),
+ None => None,
+ },
+ ts,
+ nonce: match nonce {
+ Some(nonce) => Some(nonce.to_string()),
+ None => None,
+ },
+ mac: match mac {
+ Some(mac) => Some(Mac::from(mac)),
+ None => None,
+ },
+ ext: match ext {
+ Some(ext) => Some(ext.to_string()),
+ None => None,
+ },
+ hash,
+ app: match app {
+ Some(app) => Some(app.to_string()),
+ None => None,
+ },
+ dlg: match dlg {
+ Some(dlg) => Some(dlg.to_string()),
+ None => None,
+ },
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::Header;
+ use crate::mac::Mac;
+ use std::str::FromStr;
+ use std::time::{Duration, UNIX_EPOCH};
+
+ #[test]
+ fn illegal_id() {
+ assert!(Header::new(
+ Some("ab\"cdef"),
+ Some(UNIX_EPOCH + Duration::new(1234, 0)),
+ Some("nonce"),
+ Some(Mac::from(vec![])),
+ Some("ext"),
+ None,
+ None,
+ None
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn illegal_nonce() {
+ assert!(Header::new(
+ Some("abcdef"),
+ Some(UNIX_EPOCH + Duration::new(1234, 0)),
+ Some("no\"nce"),
+ Some(Mac::from(vec![])),
+ Some("ext"),
+ None,
+ None,
+ None
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn illegal_ext() {
+ assert!(Header::new(
+ Some("abcdef"),
+ Some(UNIX_EPOCH + Duration::new(1234, 0)),
+ Some("nonce"),
+ Some(Mac::from(vec![])),
+ Some("ex\"t"),
+ None,
+ None,
+ None
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn illegal_app() {
+ assert!(Header::new(
+ Some("abcdef"),
+ Some(UNIX_EPOCH + Duration::new(1234, 0)),
+ Some("nonce"),
+ Some(Mac::from(vec![])),
+ None,
+ None,
+ Some("a\"pp"),
+ None
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn illegal_dlg() {
+ assert!(Header::new(
+ Some("abcdef"),
+ Some(UNIX_EPOCH + Duration::new(1234, 0)),
+ Some("nonce"),
+ Some(Mac::from(vec![])),
+ None,
+ None,
+ None,
+ Some("d\"lg")
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn from_str() {
+ let s = Header::from_str(
+ "id=\"dh37fgj492je\", ts=\"1353832234\", \
+ nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \
+ mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \
+ hash=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \
+ app=\"my-app\", dlg=\"my-authority\"",
+ )
+ .unwrap();
+ assert!(s.id == Some("dh37fgj492je".to_string()));
+ assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
+ assert!(s.nonce == Some("j4h3g2".to_string()));
+ assert!(
+ s.mac
+ == Some(Mac::from(vec![
+ 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
+ 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
+ ]))
+ );
+ assert!(s.ext == Some("some-app-ext-data".to_string()));
+ assert!(s.app == Some("my-app".to_string()));
+ assert!(s.dlg == Some("my-authority".to_string()));
+ }
+
+ #[test]
+ fn from_str_invalid_mac() {
+ let r = Header::from_str(
+ "id=\"dh37fgj492je\", ts=\"1353832234\", \
+ nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \
+ mac=\"6!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!AE=\", \
+ app=\"my-app\", dlg=\"my-authority\"",
+ );
+ assert!(r.is_err());
+ }
+
+ #[test]
+ fn from_str_no_field() {
+ let s = Header::from_str("").unwrap();
+ assert!(s.id == None);
+ assert!(s.ts == None);
+ assert!(s.nonce == None);
+ assert!(s.mac == None);
+ assert!(s.ext == None);
+ assert!(s.app == None);
+ assert!(s.dlg == None);
+ }
+
+ #[test]
+ fn from_str_few_field() {
+ let s = Header::from_str(
+ "id=\"xyz\", ts=\"1353832234\", \
+ nonce=\"abc\", \
+ mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"",
+ )
+ .unwrap();
+ assert!(s.id == Some("xyz".to_string()));
+ assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
+ assert!(s.nonce == Some("abc".to_string()));
+ assert!(
+ s.mac
+ == Some(Mac::from(vec![
+ 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
+ 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
+ ]))
+ );
+ assert!(s.ext == None);
+ assert!(s.app == None);
+ assert!(s.dlg == None);
+ }
+
+ #[test]
+ fn from_str_messy() {
+ let s = Header::from_str(
+ ", id = \"dh37fgj492je\", ts=\"1353832234\", \
+ nonce=\"j4h3g2\" , , ext=\"some-app-ext-data\", \
+ mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"",
+ )
+ .unwrap();
+ assert!(s.id == Some("dh37fgj492je".to_string()));
+ assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
+ assert!(s.nonce == Some("j4h3g2".to_string()));
+ assert!(
+ s.mac
+ == Some(Mac::from(vec![
+ 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
+ 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
+ ]))
+ );
+ assert!(s.ext == Some("some-app-ext-data".to_string()));
+ assert!(s.app == None);
+ assert!(s.dlg == None);
+ }
+
+ #[test]
+ fn to_str_no_fields() {
+ // must supply a type for S, since it is otherwise unused
+ let s = Header::new::<String>(None, None, None, None, None, None, None, None).unwrap();
+ let formatted = format!("{}", s);
+ println!("got: {}", formatted);
+ assert!(formatted == "")
+ }
+
+ #[test]
+ fn to_str_few_fields() {
+ let s = Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
+ 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
+ ])),
+ None,
+ None,
+ None,
+ None,
+ )
+ .unwrap();
+ let formatted = format!("{}", s);
+ println!("got: {}", formatted);
+ assert!(
+ formatted
+ == "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \
+ mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\""
+ )
+ }
+
+ #[test]
+ fn to_str_maximal() {
+ let s = Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
+ 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
+ ])),
+ Some("my-ext-value"),
+ Some(vec![1, 2, 3, 4]),
+ Some("my-app"),
+ Some("my-dlg"),
+ )
+ .unwrap();
+ let formatted = format!("{}", s);
+ println!("got: {}", formatted);
+ assert!(
+ formatted
+ == "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \
+ mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\", ext=\"my-ext-value\", \
+ hash=\"AQIDBA==\", app=\"my-app\", dlg=\"my-dlg\""
+ )
+ }
+
+ #[test]
+ fn round_trip() {
+ let s = Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
+ 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
+ ])),
+ Some("my-ext-value"),
+ Some(vec![1, 2, 3, 4]),
+ Some("my-app"),
+ Some("my-dlg"),
+ )
+ .unwrap();
+ let formatted = format!("{}", s);
+ println!("got: {}", s);
+ let s2 = Header::from_str(&formatted).unwrap();
+ assert!(s2 == s);
+ }
+}
diff --git a/third_party/rust/hawk/src/lib.rs b/third_party/rust/hawk/src/lib.rs
new file mode 100644
index 0000000000..4bde1229e2
--- /dev/null
+++ b/third_party/rust/hawk/src/lib.rs
@@ -0,0 +1,173 @@
+//! The `hawk` crate provides support for [Hawk](https://github.com/hueniverse/hawk)
+//! authentictation. It is a low-level crate, used by higher-level crates to integrate with various
+//! Rust HTTP libraries. For example `hyper-hawk` integrates Hawk with Hyper.
+//!
+//! # Examples
+//!
+//! ## Hawk Client
+//!
+//! A client can attach a Hawk Authorization header to requests by providing credentials to a
+//! Request instance, which will generate the header.
+//!
+//! ```
+//! use hawk::{RequestBuilder, Credentials, Key, SHA256, PayloadHasher};
+//! use std::time::{Duration, UNIX_EPOCH};
+//!
+//! fn main() {
+//! // provide the Hawk id and key
+//! let credentials = Credentials {
+//! id: "test-client".to_string(),
+//! key: Key::new(vec![99u8; 32], SHA256).unwrap(),
+//! };
+//!
+//! let payload_hash = PayloadHasher::hash("text/plain", SHA256, "request-body").unwrap();
+//!
+//! // provide the details of the request to be authorized
+//! let request = RequestBuilder::new("POST", "example.com", 80, "/v1/users")
+//! .hash(&payload_hash[..])
+//! .request();
+//!
+//! // Get the resulting header, including the calculated MAC; this involves a random
+//! // nonce, so the MAC will be different on every request.
+//! let header = request.make_header(&credentials).unwrap();
+//!
+//! // the header would the be attached to the request
+//! assert_eq!(header.id.unwrap(), "test-client");
+//! assert_eq!(header.mac.unwrap().len(), 32);
+//! assert_eq!(header.hash.unwrap().len(), 32);
+//! }
+//! ```
+//!
+//! A client that wishes to use a bewit (URL parameter) can do so as follows:
+//!
+//! ```
+//! use hawk::{RequestBuilder, Credentials, Key, SHA256, Bewit};
+//! use std::time::Duration;
+//! use std::borrow::Cow;
+//!
+//! let credentials = Credentials {
+//! id: "me".to_string(),
+//! key: Key::new("tok", SHA256).unwrap(),
+//! };
+//!
+//! let client_req = RequestBuilder::new("GET", "mysite.com", 443, "/resource").request();
+//! let client_bewit = client_req
+//! .make_bewit_with_ttl(&credentials, Duration::from_secs(10))
+//! .unwrap();
+//! let request_path = format!("/resource?bewit={}", client_bewit.to_str());
+//! // .. make the request
+//! ```
+//!
+//! ## Hawk Server
+//!
+//! To act as a server, parse the Hawk Authorization header from the request, generate a new
+//! Request instance, and use the request to validate the header.
+//!
+//! ```
+//! use hawk::{RequestBuilder, Header, Key, SHA256};
+//! use hawk::mac::Mac;
+//! use std::time::{Duration, UNIX_EPOCH};
+//!
+//! fn main() {
+//! let mac = Mac::from(vec![7, 22, 226, 240, 84, 78, 49, 75, 115, 144, 70,
+//! 106, 102, 134, 144, 128, 225, 239, 95, 132, 202,
+//! 154, 213, 118, 19, 63, 183, 108, 215, 134, 118, 115]);
+//! // get the header (usually from the received request; constructed directly here)
+//! let hdr = Header::new(Some("dh37fgj492je"),
+//! Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+//! Some("j4h3g2"),
+//! Some(mac),
+//! Some("my-ext-value"),
+//! Some(vec![1, 2, 3, 4]),
+//! Some("my-app"),
+//! Some("my-dlg")).unwrap();
+//!
+//! // build a request object based on what we know
+//! let hash = vec![1, 2, 3, 4];
+//! let request = RequestBuilder::new("GET", "localhost", 443, "/resource")
+//! .hash(&hash[..])
+//! .request();
+//!
+//! let key = Key::new(vec![99u8; 32], SHA256).unwrap();
+//! let one_week_in_secs = 7 * 24 * 60 * 60;
+//! if !request.validate_header(&hdr, &key, Duration::from_secs(5200 * one_week_in_secs)) {
+//! panic!("header validation failed. Is it 2117 already?");
+//! }
+//! }
+//! ```
+//!
+//! A server which validates bewits looks like this:
+//!
+//! ```
+//! use hawk::{RequestBuilder, Credentials, Key, SHA256, Bewit};
+//! use std::time::Duration;
+//! use std::borrow::Cow;
+//!
+//! let credentials = Credentials {
+//! id: "me".to_string(),
+//! key: Key::new("tok", SHA256).unwrap(),
+//! };
+//!
+//! // simulate the client generation of a bewit
+//! let client_req = RequestBuilder::new("GET", "mysite.com", 443, "/resource").request();
+//! let client_bewit = client_req
+//! .make_bewit_with_ttl(&credentials, Duration::from_secs(10))
+//! .unwrap();
+//! let request_path = format!("/resource?bewit={}", client_bewit.to_str());
+//!
+//! let mut maybe_bewit = None;
+//! let server_req = RequestBuilder::new("GET", "mysite.com", 443, &request_path)
+//! .extract_bewit(&mut maybe_bewit).unwrap()
+//! .request();
+//! let bewit = maybe_bewit.unwrap();
+//! assert_eq!(bewit.id(), "me");
+//! assert!(server_req.validate_bewit(&bewit, &credentials.key));
+//! ```
+//!
+//! ## Features
+//!
+//! By default, the `use_ring` feature is enabled, which means that this crate will
+//! use `ring` for all cryptographic operations.
+//!
+//! Alternatively, one can configure the crate with the `use_openssl`
+//! feature to use the `openssl` crate.
+//!
+//! If no features are enabled, you must provide a custom implementation of the
+//! [`hawk::crypto::Cryptographer`] trait to the `set_cryptographer` function, or
+//! the cryptographic operations will panic.
+//!
+//! Attempting to configure both the `use_ring` and `use_openssl` features will
+//! result in a build error.
+
+#[cfg(test)]
+#[macro_use]
+extern crate pretty_assertions;
+
+mod header;
+pub use crate::header::Header;
+
+mod credentials;
+pub use crate::credentials::{Credentials, DigestAlgorithm, Key};
+
+mod request;
+pub use crate::request::{Request, RequestBuilder};
+
+mod response;
+pub use crate::response::{Response, ResponseBuilder};
+
+mod error;
+pub use crate::error::*;
+
+mod payload;
+pub use crate::payload::PayloadHasher;
+
+mod bewit;
+pub use crate::bewit::Bewit;
+
+pub mod mac;
+
+pub mod crypto;
+
+pub const SHA256: DigestAlgorithm = DigestAlgorithm::Sha256;
+pub const SHA384: DigestAlgorithm = DigestAlgorithm::Sha384;
+pub const SHA512: DigestAlgorithm = DigestAlgorithm::Sha512;
diff --git a/third_party/rust/hawk/src/mac.rs b/third_party/rust/hawk/src/mac.rs
new file mode 100644
index 0000000000..d56e755c86
--- /dev/null
+++ b/third_party/rust/hawk/src/mac.rs
@@ -0,0 +1,200 @@
+use crate::credentials::Key;
+use crate::error::*;
+use base64::{display::Base64Display, STANDARD};
+use std::io::Write;
+use std::ops::Deref;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+/// The kind of MAC calcuation (corresponding to the first line of the message)
+pub enum MacType {
+ Header,
+ Response,
+ Bewit,
+}
+
+/// Mac represents a message authentication code, the signature in a Hawk transaction.
+///
+/// This class supports creating Macs using the Hawk specification, and comparing Macs
+/// using a cosntant-time comparison (thus preventing timing side-channel attacks).
+#[derive(Debug, Clone)]
+pub struct Mac(Vec<u8>);
+
+impl Mac {
+ pub fn new(
+ mac_type: MacType,
+ key: &Key,
+ ts: SystemTime,
+ nonce: &str,
+ method: &str,
+ host: &str,
+ port: u16,
+ path: &str,
+ hash: Option<&[u8]>,
+ ext: Option<&str>,
+ ) -> Result<Mac> {
+ // Note: there's a \n after each item.
+ let mut buffer: Vec<u8> = Vec::with_capacity(
+ 15 + 1 + // mac_type (worst case since it doesn't really matter)
+ 10 + 1 + // ts (in practice this will be 10 bytes)
+ nonce.len() + 1 +
+ host.len() + 1 +
+ 6 + 1 + // Longer than 6 bytes of port seems very unlikely
+ path.len() + 1 +
+ hash.map_or(0, |h| h.len() * 4 / 3) + 1 +
+ ext.map_or(0, str::len) + 1,
+ );
+
+ writeln!(
+ buffer,
+ "{mac_type}\n{ts}\n{nonce}\n{method}\n{path}\n{host}\n{port}",
+ mac_type = match mac_type {
+ MacType::Header => "hawk.1.header",
+ MacType::Response => "hawk.1.response",
+ MacType::Bewit => "hawk.1.bewit",
+ },
+ ts = ts.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
+ nonce = nonce,
+ method = method,
+ path = path,
+ host = host,
+ port = port,
+ )?;
+
+ if let Some(h) = hash {
+ writeln!(buffer, "{}", Base64Display::with_config(h, STANDARD))?;
+ } else {
+ writeln!(buffer)?;
+ }
+ writeln!(buffer, "{}", ext.unwrap_or_default())?;
+
+ Ok(Mac(key.sign(buffer.as_ref())?))
+ }
+}
+
+impl AsRef<[u8]> for Mac {
+ fn as_ref(&self) -> &[u8] {
+ &self.0[..]
+ }
+}
+
+impl From<Vec<u8>> for Mac {
+ fn from(original: Vec<u8>) -> Self {
+ Mac(original)
+ }
+}
+
+impl Deref for Mac {
+ type Target = Vec<u8>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl PartialEq for Mac {
+ fn eq(&self, other: &Mac) -> bool {
+ crate::crypto::constant_time_compare(&self.0, &other.0)
+ }
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod test {
+ use super::{Mac, MacType};
+ use crate::credentials::Key;
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+ fn key() -> Key {
+ Key::new(
+ vec![
+ 11u8, 19, 228, 209, 79, 189, 200, 59, 166, 47, 86, 254, 235, 184, 120, 197, 75,
+ 152, 201, 79, 115, 61, 111, 242, 219, 187, 173, 14, 227, 108, 60, 232,
+ ],
+ crate::SHA256,
+ )
+ .unwrap()
+ }
+
+ fn sys_time(secs: u64, ns: u32) -> SystemTime {
+ UNIX_EPOCH + Duration::new(secs, ns)
+ }
+
+ #[test]
+ fn test_make_mac() {
+ let key = key();
+ let mac = Mac::new(
+ MacType::Header,
+ &key,
+ sys_time(1000, 100),
+ "nonny",
+ "POST",
+ "mysite.com",
+ 443,
+ "/v1/api",
+ None,
+ None,
+ )
+ .unwrap();
+ println!("got {:?}", mac);
+ assert!(
+ mac.0
+ == vec![
+ 192, 227, 235, 121, 157, 185, 197, 79, 189, 214, 235, 139, 9, 232, 99, 55, 67,
+ 30, 68, 0, 150, 187, 192, 238, 21, 200, 209, 107, 245, 159, 243, 178
+ ]
+ );
+ }
+
+ #[test]
+ fn test_make_mac_hash() {
+ let key = key();
+ let hash = vec![1, 2, 3, 4, 5];
+ let mac = Mac::new(
+ MacType::Header,
+ &key,
+ sys_time(1000, 100),
+ "nonny",
+ "POST",
+ "mysite.com",
+ 443,
+ "/v1/api",
+ Some(&hash),
+ None,
+ )
+ .unwrap();
+ println!("got {:?}", mac);
+ assert!(
+ mac.0
+ == vec![
+ 61, 128, 208, 253, 88, 135, 190, 196, 1, 69, 153, 193, 124, 4, 195, 87, 38, 96,
+ 181, 34, 65, 234, 58, 157, 175, 175, 145, 151, 61, 0, 57, 5
+ ]
+ );
+ }
+
+ #[test]
+ fn test_make_mac_ext() {
+ let key = key();
+ let ext = "ext-data".to_string();
+ let mac = Mac::new(
+ MacType::Header,
+ &key,
+ sys_time(1000, 100),
+ "nonny",
+ "POST",
+ "mysite.com",
+ 443,
+ "/v1/api",
+ None,
+ Some(&ext),
+ )
+ .unwrap();
+ println!("got {:?}", mac);
+ assert!(
+ mac.0
+ == vec![
+ 187, 104, 238, 100, 168, 112, 37, 68, 187, 141, 168, 155, 177, 193, 113, 0, 50,
+ 105, 127, 36, 24, 117, 200, 251, 138, 199, 108, 14, 105, 123, 234, 119
+ ]
+ );
+ }
+}
diff --git a/third_party/rust/hawk/src/payload.rs b/third_party/rust/hawk/src/payload.rs
new file mode 100644
index 0000000000..3b872eaf4b
--- /dev/null
+++ b/third_party/rust/hawk/src/payload.rs
@@ -0,0 +1,87 @@
+use crate::error::*;
+use crate::{crypto, DigestAlgorithm};
+/// A utility for hashing payloads. Feed your entity body to this, then pass the `finish`
+/// result to a request or response.
+pub struct PayloadHasher(Box<dyn crypto::Hasher>);
+
+impl PayloadHasher {
+ /// Create a new PayloadHasher. The `content_type` should be lower-case and should
+ /// not include parameters. The digest is assumed to be the same as the digest used
+ /// for the credentials in the request.
+ pub fn new<B>(content_type: B, algorithm: DigestAlgorithm) -> Result<Self>
+ where
+ B: AsRef<[u8]>,
+ {
+ let mut hasher = PayloadHasher(crypto::new_hasher(algorithm)?);
+ hasher.update(b"hawk.1.payload\n")?;
+ hasher.update(content_type.as_ref())?;
+ hasher.update(b"\n")?;
+ Ok(hasher)
+ }
+
+ /// Hash a single value and return it
+ pub fn hash<B1, B2>(
+ content_type: B1,
+ algorithm: DigestAlgorithm,
+ payload: B2,
+ ) -> Result<Vec<u8>>
+ where
+ B1: AsRef<[u8]>,
+ B2: AsRef<[u8]>,
+ {
+ let mut hasher = PayloadHasher::new(content_type, algorithm)?;
+ hasher.update(payload)?;
+ hasher.finish()
+ }
+
+ /// Update the hash with new data.
+ pub fn update<B>(&mut self, data: B) -> Result<()>
+ where
+ B: AsRef<[u8]>,
+ {
+ self.0.update(data.as_ref())?;
+ Ok(())
+ }
+
+ /// Finish hashing and return the result
+ ///
+ /// Note that this appends a newline to the payload, as does the JS Hawk implementaiton.
+ pub fn finish(mut self) -> Result<Vec<u8>> {
+ self.update(b"\n")?;
+ Ok(self.0.finish()?)
+ }
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod tests {
+ use super::PayloadHasher;
+
+ #[test]
+ fn hash_consistency() -> super::Result<()> {
+ let mut hasher1 = PayloadHasher::new("text/plain", crate::SHA256)?;
+ hasher1.update("pày")?;
+ hasher1.update("load")?;
+ let hash1 = hasher1.finish()?;
+
+ let mut hasher2 = PayloadHasher::new("text/plain", crate::SHA256)?;
+ hasher2.update("pàyload")?;
+ let hash2 = hasher2.finish()?;
+
+ let hash3 = PayloadHasher::hash("text/plain", crate::SHA256, "pàyload")?;
+
+ let hash4 = // "pàyload" as utf-8 bytes
+ PayloadHasher::hash("text/plain", crate::SHA256, vec![112, 195, 160, 121, 108, 111, 97, 100])?;
+
+ assert_eq!(
+ hash1,
+ vec![
+ 228, 238, 241, 224, 235, 114, 158, 112, 211, 254, 118, 89, 25, 236, 87, 176, 181,
+ 54, 61, 135, 42, 223, 188, 103, 194, 59, 83, 36, 136, 31, 198, 50
+ ]
+ );
+ assert_eq!(hash2, hash1);
+ assert_eq!(hash3, hash1);
+ assert_eq!(hash4, hash1);
+ Ok(())
+ }
+}
diff --git a/third_party/rust/hawk/src/request.rs b/third_party/rust/hawk/src/request.rs
new file mode 100644
index 0000000000..4cccab20d1
--- /dev/null
+++ b/third_party/rust/hawk/src/request.rs
@@ -0,0 +1,974 @@
+use crate::bewit::Bewit;
+use crate::credentials::{Credentials, Key};
+use crate::error::*;
+use crate::header::Header;
+use crate::mac::{Mac, MacType};
+use crate::response::ResponseBuilder;
+use log::debug;
+use std::borrow::Cow;
+use std::str;
+use std::str::FromStr;
+use std::time::{Duration, SystemTime};
+use url::{Position, Url};
+
+/// Request represents a single HTTP request.
+///
+/// The structure is created using (RequestBuilder)[struct.RequestBuilder.html]. Most uses of this
+/// library will hold several of the fields in this structure fixed. Cloning the structure with
+/// these fields applied is a convenient way to avoid repeating those fields. Most fields are
+/// references, since in common use the values already exist and will outlive the request.
+///
+/// A request can be used on the client, to generate a header or a bewit, or on the server, to
+/// validate the same.
+///
+/// # Examples
+///
+/// ```
+/// use hawk::RequestBuilder;
+/// let bldr = RequestBuilder::new("GET", "mysite.com", 443, "/");
+/// let request1 = bldr.clone().method("POST").path("/api/user").request();
+/// let request2 = bldr.path("/api/users").request();
+/// ```
+///
+/// See the documentation in the crate root for examples of creating and validating headers.
+#[derive(Debug, Clone)]
+pub struct Request<'a> {
+ method: &'a str,
+ host: &'a str,
+ port: u16,
+ path: Cow<'a, str>,
+ hash: Option<&'a [u8]>,
+ ext: Option<&'a str>,
+ app: Option<&'a str>,
+ dlg: Option<&'a str>,
+}
+
+impl<'a> Request<'a> {
+ /// Create a new Header for this request, inventing a new nonce and setting the
+ /// timestamp to the current time.
+ pub fn make_header(&self, credentials: &Credentials) -> Result<Header> {
+ let nonce = random_string(10)?;
+ self.make_header_full(credentials, SystemTime::now(), nonce)
+ }
+
+ /// Similar to `make_header`, but allowing specification of the timestamp
+ /// and nonce.
+ pub fn make_header_full<S>(
+ &self,
+ credentials: &Credentials,
+ ts: SystemTime,
+ nonce: S,
+ ) -> Result<Header>
+ where
+ S: Into<String>,
+ {
+ let nonce = nonce.into();
+ let mac = Mac::new(
+ MacType::Header,
+ &credentials.key,
+ ts,
+ &nonce,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ self.ext,
+ )?;
+ Header::new(
+ Some(credentials.id.clone()),
+ Some(ts),
+ Some(nonce),
+ Some(mac),
+ match self.ext {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ match self.hash {
+ None => None,
+ Some(v) => Some(v.to_vec()),
+ },
+ match self.app {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ match self.dlg {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ )
+ }
+
+ /// Make a "bewit" that can be attached to a URL to authenticate GET access.
+ ///
+ /// The ttl gives the time for which this bewit is valid, starting now.
+ pub fn make_bewit(&self, credentials: &'a Credentials, exp: SystemTime) -> Result<Bewit<'a>> {
+ // note that this includes `method` and `hash` even though they must always be GET and None
+ // for bewits. If they aren't, then the bewit just won't validate -- no need to catch
+ // that now
+ let mac = Mac::new(
+ MacType::Bewit,
+ &credentials.key,
+ exp,
+ "",
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ self.ext,
+ )?;
+ let bewit = Bewit::new(&credentials.id, exp, mac, self.ext);
+ Ok(bewit)
+ }
+
+ /// Variant of `make_bewit` that takes a Duration (starting from now)
+ /// instead of a SystemTime, provided for convenience.
+ pub fn make_bewit_with_ttl(
+ &self,
+ credentials: &'a Credentials,
+ ttl: Duration,
+ ) -> Result<Bewit<'a>> {
+ let exp = SystemTime::now() + ttl;
+ self.make_bewit(credentials, exp)
+ }
+
+ /// Validate the given header. This validates that the `mac` field matches that calculated
+ /// using the other header fields and the given request information.
+ ///
+ /// The header's timestamp is verified to be within `ts_skew` of the current time. If any of
+ /// the required header fields are missing, the method will return false.
+ ///
+ /// It is up to the caller to examine the header's `id` field and supply the corresponding key.
+ ///
+ /// If desired, it is up to the caller to validate that `nonce` has not been used before.
+ ///
+ /// If a hash has been supplied, then the header must contain a matching hash. Note that this
+ /// hash must be calculated based on the request body, not copied from the request header!
+ pub fn validate_header(&self, header: &Header, key: &Key, ts_skew: Duration) -> bool {
+ // extract required fields, returning early if they are not present
+ let ts = match header.ts {
+ Some(ts) => ts,
+ None => {
+ debug!("missing timestamp from header");
+ return false;
+ }
+ };
+ let nonce = match header.nonce {
+ Some(ref nonce) => nonce,
+ None => {
+ debug!("missing nonce from header");
+ return false;
+ }
+ };
+ let header_mac = match header.mac {
+ Some(ref mac) => mac,
+ None => {
+ debug!("missing mac from header");
+ return false;
+ }
+ };
+ let header_hash = match header.hash {
+ Some(ref hash) => Some(&hash[..]),
+ None => None,
+ };
+ let header_ext = match header.ext {
+ Some(ref ext) => Some(&ext[..]),
+ None => None,
+ };
+
+ // first verify the MAC
+ match Mac::new(
+ MacType::Header,
+ key,
+ ts,
+ nonce,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ header_hash,
+ header_ext,
+ ) {
+ Ok(calculated_mac) => {
+ if &calculated_mac != header_mac {
+ debug!("calculated mac doesn't match header");
+ return false;
+ }
+ }
+ Err(e) => {
+ debug!("unexpected mac error: {:?}", e);
+ return false;
+ }
+ };
+
+ // ..then the hashes
+ if let Some(local_hash) = self.hash {
+ if let Some(server_hash) = header_hash {
+ if local_hash != server_hash {
+ debug!("server hash doesn't match header");
+ return false;
+ }
+ } else {
+ debug!("missing hash from header");
+ return false;
+ }
+ }
+
+ // ..then the timestamp
+ let now = SystemTime::now();
+ let skew = if now > ts {
+ now.duration_since(ts).unwrap()
+ } else {
+ ts.duration_since(now).unwrap()
+ };
+ if skew > ts_skew {
+ debug!(
+ "bad timestamp skew, timestamp too old? detected skew: {:?}, ts_skew: {:?}",
+ &skew, &ts_skew
+ );
+ return false;
+ }
+
+ true
+ }
+
+ /// Validate the given bewit matches this request.
+ ///
+ /// It is up to the caller to consult the Bewit's `id` and look up the
+ /// corresponding key.
+ ///
+ /// Nonces and hashes do not apply when using bewits.
+ pub fn validate_bewit(&self, bewit: &Bewit, key: &Key) -> bool {
+ let calculated_mac = Mac::new(
+ MacType::Bewit,
+ key,
+ bewit.exp(),
+ "",
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ match bewit.ext() {
+ Some(e) => Some(e),
+ None => None,
+ },
+ );
+ let calculated_mac = match calculated_mac {
+ Ok(m) => m,
+ Err(_) => {
+ return false;
+ }
+ };
+
+ if bewit.mac() != &calculated_mac {
+ return false;
+ }
+
+ let now = SystemTime::now();
+ if bewit.exp() < now {
+ return false;
+ }
+
+ true
+ }
+
+ /// Get a Response instance for a response to this request. This is a convenience
+ /// wrapper around `Response::from_request_header`.
+ pub fn make_response_builder(&'a self, req_header: &'a Header) -> ResponseBuilder<'a> {
+ ResponseBuilder::from_request_header(
+ req_header,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ )
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct RequestBuilder<'a>(Request<'a>);
+
+impl<'a> RequestBuilder<'a> {
+ /// Create a new request with the given method, host, port, and path.
+ pub fn new(method: &'a str, host: &'a str, port: u16, path: &'a str) -> Self {
+ RequestBuilder(Request {
+ method,
+ host,
+ port,
+ path: Cow::Borrowed(path),
+ hash: None,
+ ext: None,
+ app: None,
+ dlg: None,
+ })
+ }
+
+ /// Create a new request with the host, port, and path determined from the URL.
+ pub fn from_url(method: &'a str, url: &'a Url) -> Result<Self> {
+ let (host, port, path) = RequestBuilder::parse_url(url)?;
+ Ok(RequestBuilder(Request {
+ method,
+ host,
+ port,
+ path: Cow::Borrowed(path),
+ hash: None,
+ ext: None,
+ app: None,
+ dlg: None,
+ }))
+ }
+
+ /// Set the request method. This should be a capitalized string.
+ pub fn method(mut self, method: &'a str) -> Self {
+ self.0.method = method;
+ self
+ }
+
+ /// Set the URL path for the request.
+ pub fn path(mut self, path: &'a str) -> Self {
+ self.0.path = Cow::Borrowed(path);
+ self
+ }
+
+ /// Set the URL hostname for the request
+ pub fn host(mut self, host: &'a str) -> Self {
+ self.0.host = host;
+ self
+ }
+
+ /// Set the URL port for the request
+ pub fn port(mut self, port: u16) -> Self {
+ self.0.port = port;
+ self
+ }
+
+ /// Set the hostname, port, and path for the request, from a string URL.
+ pub fn url(self, url: &'a Url) -> Result<Self> {
+ let (host, port, path) = RequestBuilder::parse_url(url)?;
+ Ok(self.path(path).host(host).port(port))
+ }
+
+ /// Set the content hash for the request
+ 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 request
+ pub fn ext<S: Into<Option<&'a str>>>(mut self, ext: S) -> Self {
+ self.0.ext = ext.into();
+ self
+ }
+
+ /// Set the `app` Hawk property for the request
+ pub fn app<S: Into<Option<&'a str>>>(mut self, app: S) -> Self {
+ self.0.app = app.into();
+ self
+ }
+
+ /// Set the `dlg` Hawk property for the request
+ pub fn dlg<S: Into<Option<&'a str>>>(mut self, dlg: S) -> Self {
+ self.0.dlg = dlg.into();
+ self
+ }
+
+ /// Get the request from this builder
+ pub fn request(self) -> Request<'a> {
+ self.0
+ }
+
+ /// Extract the `bewit` query parameter, if any, from the path, and return it in the output
+ /// parameter, returning a modified RequestBuilder omitting the `bewit=..` query parameter. If
+ /// no bewit is present, or if an error is returned, the output parameter is reset to None.
+ ///
+ /// The path manipulation is tested to correspond to that preformed by the hueniverse/hawk
+ /// implementation-specification
+ pub fn extract_bewit(mut self, bewit: &mut Option<Bewit<'a>>) -> Result<Self> {
+ const PREFIX: &str = "bewit=";
+ *bewit = None;
+
+ if let Some(query_index) = self.0.path.find('?') {
+ let (bewit_components, components): (Vec<&str>, Vec<&str>) = self.0.path
+ [query_index + 1..]
+ .split('&')
+ .partition(|comp| comp.starts_with(PREFIX));
+
+ if bewit_components.len() == 1 {
+ let bewit_str = bewit_components[0];
+ *bewit = Some(Bewit::from_str(&bewit_str[PREFIX.len()..])?);
+
+ // update the path to omit the bewit=... segment
+ let new_path = if !components.is_empty() {
+ format!("{}{}", &self.0.path[..=query_index], components.join("&")).to_string()
+ } else {
+ // no query left, so return the remaining path, omitting the '?'
+ self.0.path[..query_index].to_string()
+ };
+ self.0.path = Cow::Owned(new_path);
+ Ok(self)
+ } else if bewit_components.is_empty() {
+ Ok(self)
+ } else {
+ Err(InvalidBewit::Multiple.into())
+ }
+ } else {
+ Ok(self)
+ }
+ }
+
+ fn parse_url(url: &'a Url) -> Result<(&'a str, u16, &'a str)> {
+ let host = url
+ .host_str()
+ .ok_or_else(|| Error::InvalidUrl(format!("url {} has no host", url)))?;
+ let port = url
+ .port_or_known_default()
+ .ok_or_else(|| Error::InvalidUrl(format!("url {} has no port", url)))?;
+ let path = &url[Position::BeforePath..];
+ Ok((host, port, path))
+ }
+}
+
+/// Create a random string with `bytes` bytes of entropy. The string
+/// is base64-encoded. so it will be longer than bytes characters.
+fn random_string(bytes: usize) -> Result<String> {
+ let mut bytes = vec![0u8; bytes];
+ crate::crypto::rand_bytes(&mut bytes)?;
+ Ok(base64::encode(&bytes))
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod test {
+ use super::*;
+ use crate::credentials::{Credentials, Key};
+ use crate::header::Header;
+ use std::str::FromStr;
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
+ use url::Url;
+
+ // this is a header from a real request using the JS Hawk library, to
+ // https://pulse.taskcluster.net:443/v1/namespaces with credentials "me" / "tok"
+ const REAL_HEADER: &'static str = "id=\"me\", ts=\"1491183061\", nonce=\"RVnYzW\", \
+ mac=\"1kqRT9EoxiZ9AA/ayOCXB+AcjfK/BoJ+n7z0gfvZotQ=\"";
+ const BEWIT_STR: &str =
+ "bWVcMTM1MzgzMjgzNFxmaXk0ZTV3QmRhcEROeEhIZUExOE5yU3JVMVUzaVM2NmdtMFhqVEpwWXlVPVw";
+
+ // this is used as the initial bewit when calling extract_bewit, to verify that it is
+ // not allowing the original value of the parameter to remain in place.
+ const INITIAL_BEWIT_STR: &str =
+ "T0ggTk9FU1wxMzUzODMyODM0XGZpeTRlNXdCZGFwRE54SEhlQTE4TnJTclUxVTNpUzY2Z20wWGpUSnBZeVU9XCZtdXQgYmV3aXQgbm90IHJlc2V0IQ";
+
+ #[test]
+ fn test_empty() {
+ let req = RequestBuilder::new("GET", "site", 80, "/").request();
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.host, "site");
+ assert_eq!(req.port, 80);
+ assert_eq!(req.path, "/");
+ assert_eq!(req.hash, None);
+ assert_eq!(req.ext, None);
+ assert_eq!(req.app, None);
+ assert_eq!(req.dlg, None);
+ }
+
+ #[test]
+ fn test_builder() {
+ let hash = vec![0u8];
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo")
+ .hash(Some(&hash[..]))
+ .ext("ext")
+ .app("app")
+ .dlg("dlg")
+ .request();
+
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443);
+ assert_eq!(req.hash, Some(&hash[..]));
+ assert_eq!(req.ext, Some("ext"));
+ assert_eq!(req.app, Some("app"));
+ assert_eq!(req.dlg, Some("dlg"));
+ }
+
+ #[test]
+ fn test_builder_clone() {
+ let rb = RequestBuilder::new("GET", "site", 443, "/foo");
+ let req = rb.clone().request();
+ let req2 = rb.path("/bar").request();
+
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req2.method, "GET");
+ assert_eq!(req2.path, "/bar");
+ }
+
+ #[test]
+ fn test_url_builder() {
+ let url = Url::parse("https://example.com/foo").unwrap();
+ let req = RequestBuilder::from_url("GET", &url).unwrap().request();
+
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_query() {
+ let url = Url::parse("https://example.com/foo?foo=bar").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?foo=bar");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_encodable_chars() {
+ let url = Url::parse("https://example.com/ñoo?foo=año").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/%C3%B1oo?foo=a%C3%B1o");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_empty_query() {
+ let url = Url::parse("https://example.com/foo?").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_alone() {
+ let url = Url::parse(&format!("https://example.com/foo?bewit={}", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo"); // NOTE: strips the `?`
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_first() {
+ let url = Url::parse(&format!("https://example.com/foo?bewit={}&a=1", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_multiple() {
+ let url = Url::parse(&format!(
+ "https://example.com/foo?bewit={}&bewit={}",
+ BEWIT_STR, BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ assert!(bldr.extract_bewit(&mut bewit).is_err());
+ assert_eq!(bewit, None);
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_invalid() {
+ let url = Url::parse("https://example.com/foo?bewit=1234").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ assert!(bldr.extract_bewit(&mut bewit).is_err());
+ assert_eq!(bewit, None);
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_last() {
+ let url = Url::parse(&format!("https://example.com/foo?a=1&bewit={}", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_middle() {
+ let url = Url::parse(&format!(
+ "https://example.com/foo?a=1&bewit={}&b=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1&b=2");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_percent_encoding() {
+ // Note that this *over*-encodes things. Perfectly legal, but the kind
+ // of thing that incautious libraries can sometimes fail to reproduce,
+ // causing Hawk validation failures
+ let url = Url::parse(&format!(
+ "https://example.com/foo?%66oo=1&bewit={}&%62ar=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?%66oo=1&%62ar=2");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_xxxbewit() {
+ // check that we're not doing a simple string search for "bewit=.."
+ let url = Url::parse(&format!(
+ "https://example.com/foo?a=1&xxxbewit={}&b=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, format!("/foo?a=1&xxxbewit={}&b=2", BEWIT_STR));
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_username_password() {
+ let url = Url::parse("https://a:b@example.com/foo?x=y").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?x=y");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_make_header_full() {
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo").request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, UNIX_EPOCH + Duration::new(1000, 100), "nonny")
+ .unwrap();
+ assert_eq!(
+ header,
+ Header {
+ id: Some("me".to_string()),
+ ts: Some(UNIX_EPOCH + Duration::new(1000, 100)),
+ nonce: Some("nonny".to_string()),
+ mac: Some(Mac::from(vec![
+ 122, 47, 2, 53, 195, 247, 185, 107, 133, 250, 61, 134, 200, 35, 118, 94, 48,
+ 175, 237, 108, 60, 71, 4, 2, 244, 66, 41, 172, 91, 7, 233, 140
+ ])),
+ ext: None,
+ hash: None,
+ app: None,
+ dlg: None,
+ }
+ );
+ }
+
+ #[test]
+ fn test_make_header_full_with_optional_fields() {
+ let hash = vec![0u8];
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo")
+ .hash(Some(&hash[..]))
+ .ext("ext")
+ .app("app")
+ .dlg("dlg")
+ .request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, UNIX_EPOCH + Duration::new(1000, 100), "nonny")
+ .unwrap();
+ assert_eq!(
+ header,
+ Header {
+ id: Some("me".to_string()),
+ ts: Some(UNIX_EPOCH + Duration::new(1000, 100)),
+ nonce: Some("nonny".to_string()),
+ mac: Some(Mac::from(vec![
+ 72, 123, 243, 214, 145, 81, 129, 54, 183, 90, 22, 136, 192, 146, 208, 53, 216,
+ 138, 145, 94, 175, 204, 217, 8, 77, 16, 202, 50, 10, 144, 133, 162
+ ])),
+ ext: Some("ext".to_string()),
+ hash: Some(hash.clone()),
+ app: Some("app".to_string()),
+ dlg: Some("dlg".to_string()),
+ }
+ );
+ }
+
+ #[test]
+ fn test_validate_matches_generated() {
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo").request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, SystemTime::now(), "nonny")
+ .unwrap();
+ assert!(req.validate_header(&header, &credentials.key, Duration::from_secs(1 * 60)));
+ }
+
+ // Well, close enough.
+ const ONE_YEAR_IN_SECS: u64 = 365 * 24 * 60 * 60;
+
+ #[test]
+ fn test_validate_real_request() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+ let req =
+ RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "/v1/namespaces").request();
+ // allow 1000 years skew, since this was a real request that
+ // happened back in 2017, when life was simple and carefree
+ assert!(req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_real_request_bad_creds() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("WRONG", crate::SHA256).unwrap(),
+ };
+ let req =
+ RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "/v1/namespaces").request();
+ assert!(!req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_real_request_bad_req_info() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+ let req = RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "WRONG PATH").request();
+ assert!(!req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ fn make_header_without_hash() -> Header {
+ Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 161, 105, 122, 110, 248, 62, 129, 193, 148, 206, 239, 193, 219, 46, 137, 221, 51,
+ 170, 135, 114, 81, 68, 145, 182, 15, 165, 145, 168, 114, 237, 52, 35,
+ ])),
+ None,
+ None,
+ None,
+ None,
+ )
+ .unwrap()
+ }
+
+ fn make_header_with_hash() -> Header {
+ Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 189, 53, 155, 244, 203, 150, 255, 238, 135, 144, 186, 93, 6, 189, 184, 21, 150,
+ 210, 226, 61, 93, 154, 17, 218, 142, 250, 254, 193, 123, 132, 131, 195,
+ ])),
+ None,
+ Some(vec![1, 2, 3, 4]),
+ None,
+ None,
+ )
+ .unwrap()
+ }
+
+ #[test]
+ fn test_validate_no_hash() {
+ let header = make_header_without_hash();
+ let req = RequestBuilder::new("", "", 0, "").request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_in_header() {
+ let header = make_header_with_hash();
+ let req = RequestBuilder::new("", "", 0, "").request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_required_but_not_given() {
+ let header = make_header_without_hash();
+ let hash = vec![1, 2, 3, 4];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(!req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_validated() {
+ let header = make_header_with_hash();
+ let hash = vec![1, 2, 3, 4];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+
+ // ..but supplying the wrong hash will cause validation to fail
+ let hash = vec![99, 99, 99, 99];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(!req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ fn round_trip_bewit(req: Request, ts: SystemTime, expected: bool) {
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+
+ let bewit = req.make_bewit(&credentials, ts).unwrap();
+
+ // convert to a string and back
+ let bewit = bewit.to_str();
+ let bewit = Bewit::from_str(&bewit).unwrap();
+
+ // and validate it maches the original request
+ assert_eq!(req.validate_bewit(&bewit, &credentials.key), expected);
+ }
+
+ #[test]
+ fn test_validate_bewit() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z").request();
+ round_trip_bewit(req, SystemTime::now() + Duration::from_secs(10 * 60), true);
+ }
+
+ #[test]
+ fn test_validate_bewit_ext() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z")
+ .ext("abcd")
+ .request();
+ round_trip_bewit(req, SystemTime::now() + Duration::from_secs(10 * 60), true);
+ }
+
+ #[test]
+ fn test_validate_bewit_expired() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z").request();
+ round_trip_bewit(req, SystemTime::now() - Duration::from_secs(10 * 60), false);
+ }
+}
diff --git a/third_party/rust/hawk/src/response.rs b/third_party/rust/hawk/src/response.rs
new file mode 100644
index 0000000000..1709225021
--- /dev/null
+++ b/third_party/rust/hawk/src/response.rs
@@ -0,0 +1,320 @@
+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 mac;
+ let ts = self.req_header.ts.ok_or(Error::MissingTs)?;
+ let nonce = self.req_header.nonce.as_ref().ok_or(Error::MissingNonce)?;
+ 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),
+ match self.ext {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ match self.hash {
+ None => None,
+ Some(v) => Some(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 = match response_header.ext {
+ Some(ref ext) => Some(&ext[..]),
+ None => None,
+ };
+ let header_hash = match response_header.hash {
+ Some(ref hash) => Some(&hash[..]),
+ None => None,
+ };
+
+ // 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()));
+ }
+}