diff options
Diffstat (limited to 'third_party/rust/hawk')
22 files changed, 3448 insertions, 0 deletions
diff --git a/third_party/rust/hawk/.cargo-checksum.json b/third_party/rust/hawk/.cargo-checksum.json new file mode 100644 index 0000000000..7ce972202a --- /dev/null +++ b/third_party/rust/hawk/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"73eb4c894a83e99234b1e6e33ea83150bdfca833f4795e1842f9d925738fd8ee","CODE_OF_CONDUCT.md":"902d5357af363426631d907e641e220b3ec89039164743f8442b3f120479b7cf","CONTRIBUTING.md":"2f395c6bff5805ada946b38d407bedea743230c845fd69cbd004da36871b9580","Cargo.toml":"cf901ec963d8dec408dea76cbc9105de87bd97cacf1061ef17c598b676cfd28f","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"43f86b667ba065459d185ec47e5a48f943ce6dfe04dc02e1cfff0baf58714243","build.rs":"ae11c573f1edf605d7a6dc740e48f2af9ec0a8c0578bf5d381d52126582fb67e","clippy.toml":"20c46fb795c2ef5317874716faa5c8ddc1ff076f3028c85c38dc560d71347ee5","src/bewit.rs":"b09d26497e3f934253578fe67026c382f3224875b30c4ce62e998af327f06a09","src/credentials.rs":"95758518cc82ecdedbc71bcea081d5e2f764e57b8e133aedcc00118b8b2a0d3a","src/crypto/holder.rs":"c0ad1269bb9b98a9f1abc17453813cc2983e958d7d3c0c95943ce74580c9fe97","src/crypto/mod.rs":"8ca9ba36f7584525f82068521dc1d8adf1a4ea95969970df155e2136e662450d","src/crypto/openssl.rs":"ced672fd59b70912095a718f112e4c02f63caf006680aa0db2f79306781d0cc9","src/crypto/ring.rs":"a6efd23f9f48596388d2242da563350cc736a5df58244796e7dbf062230a81fe","src/error.rs":"6539921e7cca19b8f62a9c2fcf5163cac872f6e537f20dc6e9b4fa6ef87aa2ae","src/header.rs":"558d7c0fc1cf83cf6ed4878d974858116e64e7c99aecf21d8af6a7a3c32739ff","src/lib.rs":"817d35fbd019f4ca6b65c581f6c5db09110d67aa0286d9503acf915904d57653","src/mac.rs":"1bd72295376cba0bfa9ebc1da7a49324380fcf243fd5dfc1ce7513aab06e7340","src/payload.rs":"fb70b564296050ff3e86d9199f0c5f2a02ebde5ca9770a95a5d174a9c2409d7b","src/request.rs":"62be42782d6a11b604c508bb6fb9bc7a5da542f7ba98cacc69142168438e6289","src/response.rs":"b0193fece1d827c3bae6a16d953d275c551951d4be3c4b76067996592b38fb1e"},"package":"7539c8d8699bae53238aacd3f93cfb0bcaef77b85dc963902b9367c5d7a84c48"}
\ No newline at end of file diff --git a/third_party/rust/hawk/CHANGELOG.md b/third_party/rust/hawk/CHANGELOG.md new file mode 100644 index 0000000000..2689ff0451 --- /dev/null +++ b/third_party/rust/hawk/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +## Unreleased Changes + +## v3.0.0 + +- The cryptography library used is now configurable. + - By default `ring` is used (the `use_ring` feature). + - You can use the `use_openssl` feature to use openssl instead + - e.g. in your Cargo.toml: + ```toml + [dependencies.hawk] + version = "..." + features = ["use_openssl"] + default-features = false + ``` + - You can use neither and provide your own implementation using the functions in + `hawk::crypto` if neither feature is enabled. + - Note that enabling both `use_ring` and `use_openssl` will cause a build + failure. + +- BREAKING: Many functions that previously returned `T` now return `hawk::Result<T>`. + - Specifically, `PayloadHasher::{hash,update,finish}`, `Key::{new,sign}`. + +- BREAKING: `hawk::SHA{256,384,512}` are now `const` `DigestAlgorithm`s and not + aliases for `ring::Algorithm` + +- BREAKING: `Key::new` now takes a `DigestAlgorithm` and not a + `&'static ring::Algorithm`. + - If you were passing e.g. `&hawk::SHA256`, you probably just need + to pass `hawk::SHA256` now instead. + +- BREAKING (though unlikely): `Error::Rng` has been removed, and `Error::Crypto` added + diff --git a/third_party/rust/hawk/CODE_OF_CONDUCT.md b/third_party/rust/hawk/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..498baa3fb0 --- /dev/null +++ b/third_party/rust/hawk/CODE_OF_CONDUCT.md @@ -0,0 +1,15 @@ +# Community Participation Guidelines + +This repository is governed by Mozilla's code of conduct and etiquette guidelines. +For more details, please read the +[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). + +## How to Report +For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. + +<!-- +## Project Specific Etiquette + +In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html). +Please update for your project. +--> diff --git a/third_party/rust/hawk/CONTRIBUTING.md b/third_party/rust/hawk/CONTRIBUTING.md new file mode 100644 index 0000000000..3ab3a82d9c --- /dev/null +++ b/third_party/rust/hawk/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We welcome pull requests from everyone. We do expect everyone to adhere to the [Mozilla Community Participation Guidelines][participation]. + +If you're trying to figure out what to work on, here are some places to find suitable projects: +* [Good first bugs][goodfirstbug]: these are scoped to make it easy for first-time contributors to get their feet wet with Taskcluster code. +* [Mentored bugs][bugsahoy]: these are slightly more involved projects that may require insight or guidance from someone on the Taskcluster team. +* [Full list of open issues][issues]: everything else + +If the project you're interested in working on isn't covered by a bug or issue, or you're unsure about how to proceed on an existing issue, it's a good idea to talk to someone on the Taskcluster team before you go too far down a particular path. You can find us in the #taskcluster channel on [Mozilla's IRC server][irc] to discuss. You can also simply add a comment to the issue or bug. + +Once you've found an issue to work on and written a patch, submit a pull request. Some things that will increase the chance that your pull request is accepted: + +* Follow our [best practices][bestpractices]. +* This includes [writing or updating tests][testing]. +* Write a [good commit message][commit]. + +Welcome to the team! + +[participation]: https://www.mozilla.org/en-US/about/governance/policies/participation/ +[issues]: ../../issues +[bugsahoy]: https://www.joshmatthews.net/bugsahoy/?taskcluster=1 +[goodfirstbug]: http://www.joshmatthews.net/bugsahoy/?taskcluster=1&simple=1 +[irc]: https://wiki.mozilla.org/IRC +[bestpractices]: https://docs.taskcluster.net/docs/manual/design/devel/best-practices +[testing]: https://docs.taskcluster.net/docs/manual/design/devel/best-practices/testing +[commit]: https://docs.taskcluster.net/docs/manual/design/devel/best-practices/commits + diff --git a/third_party/rust/hawk/Cargo.toml b/third_party/rust/hawk/Cargo.toml new file mode 100644 index 0000000000..cffced663f --- /dev/null +++ b/third_party/rust/hawk/Cargo.toml @@ -0,0 +1,57 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "hawk" +version = "3.2.1" +authors = ["Jonas Finnemann Jensen <jopsen@gmail.com>", "Dustin J. Mitchell <dustin@mozilla.com>"] +build = "build.rs" +exclude = ["docker/*", ".taskcluster.yml", ".git*"] +description = "Hawk Implementation for Rust" +homepage = "https://docs.rs/hawk/" +documentation = "https://docs.rs/hawk/" +readme = "README.md" +license = "MPL-2.0" +repository = "https://github.com/taskcluster/rust-hawk" +[dependencies.anyhow] +version = "1.0" + +[dependencies.base64] +version = "0.12" + +[dependencies.log] +version = "0.4" + +[dependencies.once_cell] +version = "1.4" + +[dependencies.openssl] +version = "0.10.20" +optional = true + +[dependencies.ring] +version = "0.16.0" +optional = true + +[dependencies.thiserror] +version = "1.0" + +[dependencies.url] +version = "2.1" +[dev-dependencies.pretty_assertions] +version = "^0.6.1" + +[features] +default = ["use_ring"] +use_openssl = ["openssl"] +use_ring = ["ring"] diff --git a/third_party/rust/hawk/LICENSE b/third_party/rust/hawk/LICENSE new file mode 100644 index 0000000000..a612ad9813 --- /dev/null +++ b/third_party/rust/hawk/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/third_party/rust/hawk/README.md b/third_party/rust/hawk/README.md new file mode 100644 index 0000000000..f08fb5e013 --- /dev/null +++ b/third_party/rust/hawk/README.md @@ -0,0 +1,4 @@ +Hawk Authentication for Rust +============================ + +This is a Rust implementation of [Hawk](https://github.com/hueniverse/hawk). diff --git a/third_party/rust/hawk/build.rs b/third_party/rust/hawk/build.rs new file mode 100644 index 0000000000..a5e367f69b --- /dev/null +++ b/third_party/rust/hawk/build.rs @@ -0,0 +1,7 @@ +// Just check that we aren't asked to use an impossible configuration. +fn main() { + assert!( + !(cfg!(feature = "use_ring") && cfg!(feature = "use_openssl")), + "Cannot configure `hawk` with both `use_ring` and `use_openssl`!" + ); +} diff --git a/third_party/rust/hawk/clippy.toml b/third_party/rust/hawk/clippy.toml new file mode 100644 index 0000000000..33644af491 --- /dev/null +++ b/third_party/rust/hawk/clippy.toml @@ -0,0 +1,2 @@ +# hawk headers have a lot of fields, and we pass them positionally.. +too-many-arguments-threshold = 15 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())); + } +} |