From 4b2b63143c46909d386afcfe8b43495caa124d3d Mon Sep 17 00:00:00 2001 From: asonix Date: Sat, 21 Sep 2019 11:26:11 -0500 Subject: [PATCH] Prepare for release --- README.md | 54 +++ http-signature-normalization-actix/Cargo.toml | 6 +- http-signature-normalization-actix/LICENSE | 417 ++++++++++++++++++ http-signature-normalization-actix/README.md | 191 ++++++++ .../examples/server.rs | 6 +- .../src/create.rs | 22 + .../src/digest/middleware.rs | 29 ++ .../src/digest/mod.rs | 37 +- http-signature-normalization-actix/src/lib.rs | 207 ++++++++- .../src/middleware.rs | 37 +- .../src/create.rs | 19 +- http-signature-normalization-http/src/lib.rs | 53 ++- src/create.rs | 18 + src/lib.rs | 114 ++++- src/verify.rs | 69 +++ 15 files changed, 1227 insertions(+), 52 deletions(-) create mode 100644 http-signature-normalization-actix/LICENSE create mode 100644 http-signature-normalization-actix/README.md diff --git a/README.md b/README.md index 55725d2..6f66c50 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ # HTTP Signature Normaliztion _An HTTP Signatures library that leaves the signing to you_ + +- [crates.io](https://crates.io/crates/http-signature-normalization) +- [docs.rs](https://docs.rs/http-signature-normalization) +- [Join the discussion on Matrix](https://matrix.to/#/!IRQaBCMWKbpBWKjQgx:asonix.dog?via=asonix.dog) + +Http Signature Normalization is a minimal-dependency crate for producing HTTP Signatures with user-provided signing and verification. The API is simple; there's a series of steps for creation and verification with types that ensure reasonable usage. + +```rust +use chrono::Duration; +use http_signature_normalization::Config; + +fn main() -> Result<(), Box> { + let config = Config { + expires_after: Duation::secs(5), + }; + + let headers = BTreeMap::new(); + + let signature_header_value = config + .begin_sign("GET", "/foo?bar=baz", headers) + .sign("my-key-id".to_owned(), |signing_string| { + // sign the string here + Ok(signing_string.to_owned()) as Result<_, Box> + })? + .signature_header(); + + let mut headers = BTreeMap::new(); + headers.insert("Signature".to_owned(), signature_header_value); + + let verified = config + .begin_verify("GET", "/foo?bar=baz", headers)? + .verify(|sig, signing_string| { + // Verify the signature here + sig == signing_string + }); + + assert!(verified) +} +``` + +### Contributing +Unless otherwise stated, all contributions to this project will be licensed under the CSL with +the exceptions listed in the License section of this file. + +### License +This work is licensed under the Cooperative Software License. This is not a Free Software +License, but may be considered a "source-available License." For most hobbyists, self-employed +developers, worker-owned companies, and cooperatives, this software can be used in most +projects so long as this software is distributed under the terms of the CSL. For more +information, see the provided LICENSE file. If none exists, the license can be found online +[here](https://lynnesbian.space/csl/). If you are a free software project and wish to use this +software under the terms of the GNU Affero General Public License, please contact me at +[asonix@asonix.dog](mailto:asonix@asonix.dog) and we can sort that out. If you wish to use this +project under any other license, especially in proprietary software, the answer is likely no. diff --git a/http-signature-normalization-actix/Cargo.toml b/http-signature-normalization-actix/Cargo.toml index f6eba8b..910f64f 100644 --- a/http-signature-normalization-actix/Cargo.toml +++ b/http-signature-normalization-actix/Cargo.toml @@ -3,13 +3,13 @@ name = "http-signature-normalization-actix" description = "An HTTP Signatures library that leaves the signing to you" version = "0.1.0" authors = ["asonix "] -license-file = "../LICENSE" -readme = "../README.md" +license-file = "LICENSE" +readme = "README.md" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = [] +default = ["sha-2", "sha-3"] digest = ["base64"] sha-2 = ["digest", "sha2"] sha-3 = ["digest", "sha3"] diff --git a/http-signature-normalization-actix/LICENSE b/http-signature-normalization-actix/LICENSE new file mode 100644 index 0000000..c4ecef9 --- /dev/null +++ b/http-signature-normalization-actix/LICENSE @@ -0,0 +1,417 @@ +Http Signature Normalization +Copyright Riley Trautman 2019 + +COOPERATIVE SOFTWARE LICENSE + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS +COPYFARLEFT PUBLIC LICENSE ("LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN +AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY +EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE +MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN AS CONSIDERATION FOR ACCEPTING THE TERMS AND +CONDITIONS OF THIS LICENSE AND FOR AGREEING TO BE BOUND BY THE TERMS +AND CONDITIONS OF THIS LICENSE. + +1. DEFINITIONS + + a. "Adaptation" means a work based upon the Work, or upon the + Work and other pre-existing works, such as a translation, + adaptation, derivative work, arrangement of music or other + alterations of a literary or artistic work, or phonogram or + performance and includes cinematographic adaptations or any + other form in which the Work may be recast, transformed, or + adapted including in any form recognizably derived from the + original, except that a work that constitutes a Collection will + not be considered an Adaptation for the purpose of this License. + For the avoidance of doubt, where the Work is a musical work, + performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be + considered an Adaptation for the purpose of this License. + + b. "Collection" means a collection of literary or artistic + works, such as encyclopedias and anthologies, or performances, + phonograms or broadcasts, or other works or subject matter other + than works listed in Section 1(f) below, which, by reason of the + selection and arrangement of their contents, constitute + intellectual creations, in which the Work is included in its + entirety in unmodified form along with one or more other + contributions, each constituting separate and independent works + in themselves, which together are assembled into a collective + whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of + this License. + + c. "Distribute" means to make available to the public the + original and copies of the Work or Adaptation, as appropriate, + through sale, gift or any other transfer of possession or + ownership. + + d. "Licensor" means the individual, individuals, entity or + entities that offer(s) the Work under the terms of this License. + + e. "Original Author" means, in the case of a literary or + artistic work, the individual, individuals, entity or entities + who created the Work or if no individual or entity can be + identified, the publisher; and in addition (i) in the case of a + performance the actors, singers, musicians, dancers, and other + persons who act, sing, deliver, declaim, play in, interpret or + otherwise perform literary or artistic works or expressions of + folklore; (ii) in the case of a phonogram the producer being the + person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of + broadcasts, the organization that transmits the broadcast. + + f. "Work" means the literary and/or artistic work offered under + the terms of this License including without limitation any + production in the literary, scientific and artistic domain, + whatever may be the mode or form of its expression including + digital form, such as a book, pamphlet and other writing; a + lecture, address, sermon or other work of the same nature; a + dramatic or dramatico-musical work; a choreographic work or + entertainment in dumb show; a musical composition with or + without words; a cinematographic work to which are assimilated + works expressed by a process analogous to cinematography; a work + of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of + applied art; an illustration, map, plan, sketch or + three-dimensional work relative to geography, topography, + architecture or science; a performance; a broadcast; a + phonogram; a compilation of data to the extent it is protected + as a copyrightable work; or a work performed by a variety or + circus performer to the extent it is not otherwise considered a + literary or artistic work. + + g. "You" means an individual or entity exercising rights under + this License who has not previously violated the terms of this + License with respect to the Work, or who has received express + permission from the Licensor to exercise rights under this + License despite a previous violation. + + h. "Publicly Perform" means to perform public recitations of the + Work and to communicate to the public those public recitations, + by any means or process, including by wire or wireless means or + public digital performances; to make available to the public + Works in such a way that members of the public may access these + Works from a place and at a place individually chosen by them; + to perform the Work to the public by any means or process and + the communication to the public of the performances of the Work, + including by public digital performance; to broadcast and + rebroadcast the Work by any means including signs, sounds or + images. + + i. "Reproduce" means to make copies of the Work by any means + including without limitation by sound or visual recordings and + the right of fixation and reproducing fixations of the Work, + including storage of a protected performance or phonogram in + digital form or other electronic medium. + + j. "Software" means any digital Work which, through use of a + third-party piece of Software or through the direct usage of + itself on a computer system, the memory of the computer is + modified dynamically or semi-dynamically. "Software", + secondly, processes or interprets information. + + k. "Source Code" means the human-readable form of Software + through which the Original Author and/or Distributor originally + created, derived, and/or modified it. + + l. "Web Service" means the use of a piece of Software to + interpret or modify information that is subsequently and directly + served to users over the Internet. + +2. FAIR DEALING RIGHTS + + Nothing in this License is intended to reduce, limit, or restrict any + uses free from copyright or rights arising from limitations or + exceptions that are provided for in connection with the copyright + protection under copyright law or other applicable laws. + +3. LICENSE GRANT + + Subject to the terms and conditions of this License, Licensor hereby + grants You a worldwide, royalty-free, non-exclusive, perpetual (for the + duration of the applicable copyright) license to exercise the rights in + the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or + more Collections, and to Reproduce the Work as incorporated in + the Collections; + + b. to create and Reproduce Adaptations provided that any such + Adaptation, including any translation in any medium, takes + reasonable steps to clearly label, demarcate or otherwise + identify that changes were made to the original Work. For + example, a translation could be marked "The original work was + translated from English to Spanish," or a modification could + indicate "The original work has been modified."; + + c. to Distribute and Publicly Perform the Work including as + incorporated in Collections; and, + + d. to Distribute and Publicly Perform Adaptations. The above + rights may be exercised in all media and formats whether now + known or hereafter devised. The above rights include the right + to make such modifications as are technically necessary to + exercise the rights in other media and formats. Subject to + Section 8(g), all rights not expressly granted by Licensor are + hereby reserved, including but not limited to the rights set + forth in Section 4(h). + +4. RESTRICTIONS + + The license granted in Section 3 above is expressly made subject to and + limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under + the terms of this License. You must include a copy of, or the + Uniform Resource Identifier (URI) for, this License with every + copy of the Work You Distribute or Publicly Perform. You may not + offer or impose any terms on the Work that restrict the terms of + this License or the ability of the recipient of the Work to + exercise the rights granted to that recipient under the terms of + the License. You may not sublicense the Work. You must keep + intact all notices that refer to this License and to the + disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of + the Work from You to exercise the rights granted to that + recipient under the terms of the License. This Section 4(a) + applies to the Work as incorporated in a Collection, but this + does not require the Collection apart from the Work itself to be + made subject to the terms of this License. If You create a + Collection, upon notice from any Licensor You must, to the + extent practicable, remove from the Collection any credit as + required by Section 4(f), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the + extent practicable, remove from the Adaptation any credit as + required by Section 4(f), as requested. + + b. Subject to the exception in Section 4(e), you may not + exercise any of the rights granted to You in Section 3 above in + any manner that is primarily intended for or directed toward + commercial advantage or private monetary compensation. The + exchange of the Work for other copyrighted works by means of + digital file-sharing or otherwise shall not be considered to be + intended for or directed toward commercial advantage or private + monetary compensation, provided there is no payment of any + monetary compensation in connection with the exchange of + copyrighted works. + + c. If the Work meets the definition of Software, You may exercise + the rights granted in Section 3 only if You provide a copy of the + corresponding Source Code from which the Work was derived in digital + form, or You provide a URI for the corresponding Source Code of + the Work, to any recipients upon request. + + d. If the Work is used as or for a Web Service, You may exercise + the rights granted in Section 3 only if You provide a copy of the + corresponding Source Code from which the Work was derived in digital + form, or You provide a URI for the corresponding Source Code to the + Work, to any recipients of the data served or modified by the Web + Service. + + e. You may exercise the rights granted in Section 3 for + commercial purposes only if you satisfy any of the following: + + i. You are a worker-owned business or worker-owned + collective; and + ii. after tax, all financial gain, surplus, profits and + benefits produced by the business or collective are + distributed among the worker-owners + iii. You are not using such rights on behalf of a business + other than those specified in 4(e.i) and elaborated upon in + 4(e.ii), nor are using such rights as a proxy on behalf of a + business with the intent to circumvent the aforementioned + restrictions on such a business. + + f. Any use by a business that is privately owned and managed, + and that seeks to generate profit from the labor of employees + paid by salary or other wages, is not permitted under this + license. + + g. If You Distribute, or Publicly Perform the Work or any + Adaptations or Collections, You must, unless a request has been + made pursuant to Section 4(a), keep intact all copyright notices + for the Work and provide, reasonable to the medium or means You + are utilizing: (i) the name of the Original Author (or + pseudonym, if applicable) if supplied, and/or if the Original + Author and/or Licensor designate another party or parties (e.g., + a sponsor institute, publishing entity, journal) for attribution + ("Attribution Parties") in Licensor!s copyright notice, terms of + service or by other reasonable means, the name of such party or + parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does + not refer to the copyright notice or licensing information for + the Work; and, (iv) consistent with Section 3(b), in the case of + an Adaptation, a credit identifying the use of the Work in the + Adaptation (e.g., "French translation of the Work by Original + Author," or "Screenplay based on original Work by Original + Author"). The credit required by this Section 4(f) may be + implemented in any reasonable manner; provided, however, that in + the case of a Adaptation or Collection, at a minimum such credit + will appear, if a credit for all contributing authors of the + Adaptation or Collection appears, then as part of these credits + and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may + only use the credit required by this Section for the purpose of + attribution in the manner set out above and, by exercising Your + rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement + by the Original Author, Licensor and/or Attribution Parties, as + appropriate, of You or Your use of the Work, without the + separate, express prior written permission of the Original + Author, Licensor and/or Attribution Parties. + + h. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those + jurisdictions in which the right to collect royalties + through any statutory or compulsory licensing scheme + cannot be waived, the Licensor reserves the exclusive + right to collect such royalties for any exercise by You of + the rights granted under this License; + + ii. Waivable Compulsory License Schemes. In those + jurisdictions in which the right to collect royalties + through any statutory or compulsory licensing scheme can + be waived, the Licensor reserves the exclusive right to + collect such royalties for any exercise by You of the + rights granted under this License if Your exercise of such + rights is for a purpose or use which is otherwise than + noncommercial as permitted under Section 4(b) and + otherwise waives the right to collect royalties through + any statutory or compulsory licensing scheme; and, + iii.Voluntary License Schemes. The Licensor reserves the + right to collect royalties, whether individually or, in + the event that the Licensor is a member of a collecting + society that administers voluntary licensing schemes, via + that society, from any exercise by You of the rights + granted under this License that is for a purpose or use + which is otherwise than noncommercial as permitted under + Section 4(b). + + i. Except as otherwise agreed in writing by the Licensor or as + may be otherwise permitted by applicable law, if You Reproduce, + Distribute or Publicly Perform the Work either by itself or as + part of any Adaptations or Collections, You must not distort, + mutilate, modify or take other derogatory action in relation to + the Work which would be prejudicial to the Original Author's + honor or reputation. Licensor agrees that in those jurisdictions + (e.g. Japan), in which any exercise of the right granted in + Section 3(b) of this License (the right to make Adaptations) + would be deemed to be a distortion, mutilation, modification or + other derogatory action prejudicial to the Original Author's + honor and reputation, the Licensor will waive or not assert, as + appropriate, this Section, to the fullest extent permitted by + the applicable national law, to enable You to reasonably + exercise Your right under Section 3(b) of this License (right to + make Adaptations) but not otherwise. + +5. REPRESENTATIONS, WARRANTIES AND DISCLAIMER + + UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR + OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY + KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, + INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, + FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF + LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF + ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW + THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO + YOU. + +6. LIMITATION ON LIABILITY + + EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL + LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF + THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED + OF THE POSSIBILITY OF SUCH DAMAGES. + +7. TERMINATION + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this + License. Individuals or entities who have received Adaptations + or Collections from You under this License, however, will not + have their licenses terminated provided such individuals or + entities remain in full compliance with those licenses. Sections + 1, 2, 5, 6, 7, and 8 will survive any termination of this + License. + + b. Subject to the above terms and conditions, the license + granted here is perpetual (for the duration of the applicable + copyright in the Work). Notwithstanding the above, Licensor + reserves the right to release the Work under different license + terms or to stop distributing the Work at any time; provided, + however that any such election will not serve to withdraw this + License (or any other license that has been, or is required to + be, granted under the terms of this License), and this License + will continue in full force and effect unless terminated as + stated above. + +8. MISCELLANEOUS + + a. Each time You Distribute or Publicly Perform the Work or a + Collection, the Licensor offers to the recipient a license to + the Work on the same terms and conditions as the license granted + to You under this License. + + b. Each time You Distribute or Publicly Perform an Adaptation, + Licensor offers to the recipient a license to the original Work + on the same terms and conditions as the license granted to You + under this License. + + c. If the Work is classified as Software, each time You Distribute + or Publicly Perform an Adaptation, Licensor offers to the recipient + a copy and/or URI of the corresponding Source Code on the same + terms and conditions as the license granted to You under this License. + + d. If the Work is used as a Web Service, each time You Distribute + or Publicly Perform an Adaptation, or serve data derived from the + Software, the Licensor offers to any recipients of the data a copy + and/or URI of the corresponding Source Code on the same terms and + conditions as the license granted to You under this License. + + e. If any provision of this License is invalid or unenforceable + under applicable law, it shall not affect the validity or + enforceability of the remainder of the terms of this License, + and without further action by the parties to this agreement, + such provision shall be reformed to the minimum extent necessary + to make such provision valid and enforceable. + + f. No term or provision of this License shall be deemed waived + and no breach consented to unless such waiver or consent shall + be in writing and signed by the party to be charged with such + waiver or consent. + + g. This License constitutes the entire agreement between the + parties with respect to the Work licensed here. There are no + understandings, agreements or representations with respect to + the Work not specified here. Licensor shall not be bound by any + additional provisions that may appear in any communication from + You. This License may not be modified without the mutual written + agreement of the Licensor and You. + + h. The rights granted under, and the subject matter referenced, + in this License were drafted utilizing the terminology of the + Berne Convention for the Protection of Literary and Artistic + Works (as amended on September 28, 1979), the Rome Convention of + 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances + and Phonograms Treaty of 1996 and the Universal Copyright + Convention (as revised on July 24, 1971). These rights and + subject matter take effect in the relevant jurisdiction in which + the License terms are sought to be enforced according to the + corresponding provisions of the implementation of those treaty + provisions in the applicable national law. If the standard suite + of rights granted under applicable copyright law includes + additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights + under applicable law. + + diff --git a/http-signature-normalization-actix/README.md b/http-signature-normalization-actix/README.md new file mode 100644 index 0000000..f93c437 --- /dev/null +++ b/http-signature-normalization-actix/README.md @@ -0,0 +1,191 @@ +# HTTP Signature Normaliztion Actix +_An HTTP Signatures library that leaves the signing to you_ + +- [crates.io](https://crates.io/crates/http-signature-normalization-actix) +- [docs.rs](https://docs.rs/http-signature-normalization-actix) +- [Join the discussion on Matrix](https://matrix.to/#/!IRQaBCMWKbpBWKjQgx:asonix.dog?via=asonix.dog) + +Http Signature Normalization is a minimal-dependency crate for producing HTTP Signatures with user-provided signing and verification. The API is simple; there's a series of steps for creation and verification with types that ensure reasonable usage. + +## Usage + +This crate provides extensions the ClientRequest type from Actix Web, and provides middlewares for verifying HTTP Signatures, and optionally, Digest headers + +#### First, add this crate to your dependencies +```toml +actix = "0.8" +actix-web = "1.0" +failure = "0.1" +http-signature-normalization-actix = { version = "0.1", default-features = false, features = ["sha2"] } +sha2 = "0.8" +``` + +#### Then, use it in your client + +```rust +use actix::System; +use actix_web::client::Client; +use failure::Fail; +use futures::future::{lazy, Future}; +use http_signature_normalization_actix::prelude::*; +use sha2::{Digest, Sha256}; + +fn main() { + System::new("client-example") + .block_on(lazy(|| { + let config = Config::default(); + let mut digest = Sha256::new(); + + Client::default() + .post("http://127.0.0.1:8010/") + .header("User-Agent", "Actix Web") + .authorization_signature_with_digest( + &config, + "my-key-id", + &mut digest, + "Hewwo-owo", + |s| Ok(base64::encode(s)) as Result<_, MyError>, + ) + .unwrap() + .send() + .map_err(|_| ()) + .and_then(|mut res| res.body().map_err(|_| ())) + .map(|body| { + println!("{:?}", body); + }) + })) + .unwrap(); +} + +#[derive(Debug, Fail)] +pub enum MyError { + #[fail(display = "Failed to read header, {}", _0)] + Convert(#[cause] ToStrError), + + #[fail(display = "Failed to create header, {}", _0)] + Header(#[cause] InvalidHeaderValue), +} + +impl From for MyError { + fn from(e: ToStrError) -> Self { + MyError::Convert(e) + } +} + +impl From for MyError { + fn from(e: InvalidHeaderValue) -> Self { + MyError::Header(e) + } +} +``` + +#### Or, use it in your server + +```rust +use actix::System; +use actix_web::{web, App, HttpResponse, HttpServer, ResponseError}; +use failure::Fail; +use http_signature_normalization_actix::{prelude::*, verify::Algorithm}; +use sha2::{Digest, Sha256}; + +#[derive(Clone, Debug)] +struct MyVerify; + +impl SignatureVerify for MyVerify { + type Error = MyError; + type Future = Result; + + fn signature_verify( + &mut self, + algorithm: Option, + key_id: &str, + signature: &str, + signing_string: &str, + ) -> Self::Future { + match algorithm { + Some(Algorithm::Hs2019) => (), + _ => return Err(MyError::Algorithm), + }; + + if key_id != "my-key-id" { + return Err(MyError::Key); + } + + let decoded = base64::decode(signature).map_err(|_| MyError::Decode)?; + + Ok(decoded == signing_string.as_bytes()) + } +} + +fn index(_: (DigestVerified, SignatureVerified)) -> &'static str { + "Eyyyyup" +} + +fn main() -> Result<(), Box> { + let sys = System::new("server-example"); + + let config = Config::default(); + + HttpServer::new(move || { + App::new() + .wrap(VerifyDigest::new(Sha256::new()).optional()) + .wrap( + VerifySignature::new(MyVerify, config.clone()) + .authorization() + .optional(), + ) + .route("/", web::post().to(index)) + }) + .bind("127.0.0.1:8010")? + .start(); + + sys.run()?; + Ok(()) +} + +#[derive(Debug, Fail)] +enum MyError { + #[fail(display = "Failed to verify, {}", _0)] + Verify(#[cause] PrepareVerifyError), + + #[fail(display = "Unsupported algorithm")] + Algorithm, + + #[fail(display = "Couldn't decode signature")] + Decode, + + #[fail(display = "Invalid key")] + Key, +} + +impl ResponseError for MyError { + fn error_response(&self) -> HttpResponse { + HttpResponse::BadRequest().finish() + } + + fn render_response(&self) -> HttpResponse { + self.error_response() + } +} + +impl From for MyError { + fn from(e: PrepareVerifyError) -> Self { + MyError::Verify(e) + } +} +``` + +### Contributing +Unless otherwise stated, all contributions to this project will be licensed under the CSL with +the exceptions listed in the License section of this file. + +### License +This work is licensed under the Cooperative Software License. This is not a Free Software +License, but may be considered a "source-available License." For most hobbyists, self-employed +developers, worker-owned companies, and cooperatives, this software can be used in most +projects so long as this software is distributed under the terms of the CSL. For more +information, see the provided LICENSE file. If none exists, the license can be found online +[here](https://lynnesbian.space/csl/). If you are a free software project and wish to use this +software under the terms of the GNU Affero General Public License, please contact me at +[asonix@asonix.dog](mailto:asonix@asonix.dog) and we can sort that out. If you wish to use this +project under any other license, especially in proprietary software, the answer is likely no. diff --git a/http-signature-normalization-actix/examples/server.rs b/http-signature-normalization-actix/examples/server.rs index 313a1e6..5fae4e5 100644 --- a/http-signature-normalization-actix/examples/server.rs +++ b/http-signature-normalization-actix/examples/server.rs @@ -62,7 +62,7 @@ fn main() -> Result<(), Box> { #[derive(Debug, Fail)] enum MyError { #[fail(display = "Failed to verify, {}", _0)] - Verify(#[cause] VerifyError), + Verify(#[cause] PrepareVerifyError), #[fail(display = "Unsupported algorithm")] Algorithm, @@ -84,8 +84,8 @@ impl ResponseError for MyError { } } -impl From for MyError { - fn from(e: VerifyError) -> Self { +impl From for MyError { + fn from(e: PrepareVerifyError) -> Self { MyError::Verify(e) } } diff --git a/http-signature-normalization-actix/src/create.rs b/http-signature-normalization-actix/src/create.rs index d4c15a3..9e59e2a 100644 --- a/http-signature-normalization-actix/src/create.rs +++ b/http-signature-normalization-actix/src/create.rs @@ -1,16 +1,29 @@ +//! Types for signing requests with Actix Web + use actix_web::http::header::{ HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue, AUTHORIZATION, }; +/// A thin wrapper around the underlying library's Signed type +/// +/// This type can add signatures to Actix Web's HeaderMap pub struct Signed { + /// The inner Signed type + /// + /// This type can produce Strings representing the Authorization or Signature headers pub signed: http_signature_normalization::create::Signed, } +/// A thin wrapper around the underlying library's Unsigned type +/// +/// This is used to prodice the proper Signed type pub struct Unsigned { + /// The inner Unsigned type pub unsigned: http_signature_normalization::create::Unsigned, } impl Signed { + /// Add the Signature Header to a given HeaderMap pub fn signature_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { let sig_header = self.signed.signature_header(); hm.insert( @@ -21,6 +34,7 @@ impl Signed { Ok(()) } + /// Add the Authorization Header to a give HeaderMap pub fn authorization_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { let auth_header = self.signed.authorization_header(); hm.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header)?); @@ -29,6 +43,14 @@ impl Signed { } impl Unsigned { + /// Sign the signing_string for the request + /// + /// ```rust,ignore + /// let signed = unsigned.sign("my-key-id".to_owned(), |signing_string| { + /// let signature = private_key.sign(signing_string)?; + /// Ok(base64::encode(signature)) + /// })?; + /// ``` pub fn sign(self, key_id: String, f: F) -> Result where F: FnOnce(&str) -> Result, diff --git a/http-signature-normalization-actix/src/digest/middleware.rs b/http-signature-normalization-actix/src/digest/middleware.rs index 5263a98..57dddc4 100644 --- a/http-signature-normalization-actix/src/digest/middleware.rs +++ b/http-signature-normalization-actix/src/digest/middleware.rs @@ -1,3 +1,5 @@ +//! Types for setting up Digest middleware verification + use actix_web::{ dev::{Body, Payload, Service, ServiceRequest, ServiceResponse, Transform}, error::PayloadError, @@ -16,21 +18,48 @@ use std::{cell::RefCell, rc::Rc}; use super::{DigestPart, DigestVerify}; #[derive(Copy, Clone, Debug)] +/// A type implementing FromRequest that can be used in route handler to guard for verified +/// digests +/// +/// This is only required when the [`VerifyDigest`] middleware is set to optional pub struct DigestVerified; + +/// The VerifyDigest middleware +/// +/// ```rust,ignore +/// let middleware = VerifyDigest::new(MyVerify::new()) +/// .optional(); +/// +/// HttpServer::new(move || { +/// App::new() +/// .wrap(middleware.clone()) +/// .route("/protected", web::post().to(|_: DigestVerified| "Verified Digest Header")) +/// .route("/unprotected", web::post().to(|| "No verification required")) +/// }) +/// ``` pub struct VerifyDigest(bool, T); + +#[doc(hidden)] pub struct VerifyMiddleware(Rc>, bool, T); + #[derive(Debug, Fail)] #[fail(display = "Error verifying digest")] +#[doc(hidden)] pub struct VerifyError; impl VerifyDigest where T: DigestVerify + Clone, { + /// Produce a new VerifyDigest with a user-provided [`Digestverify`] type pub fn new(verify_digest: T) -> Self { VerifyDigest(true, verify_digest) } + /// Mark verifying the Digest as optional + /// + /// If a digest is present in the request, it will be verified, but it is not required to be + /// present pub fn optional(self) -> Self { VerifyDigest(false, self.1) } diff --git a/http-signature-normalization-actix/src/digest/mod.rs b/http-signature-normalization-actix/src/digest/mod.rs index d27a486..35d6a9f 100644 --- a/http-signature-normalization-actix/src/digest/mod.rs +++ b/http-signature-normalization-actix/src/digest/mod.rs @@ -1,3 +1,8 @@ +//! Types and Traits for creating and verifying Digest headers +//! +//! Digest headers are commonly used in conjunction with HTTP Signatures to verify the whole +//! request when request bodies are present + use actix_web::{ client::{ClientRequest, ClientResponse}, error::PayloadError, @@ -11,23 +16,36 @@ use crate::{Config, Sign}; pub mod middleware; #[cfg(feature = "sha-2")] -pub mod sha2; +mod sha2; #[cfg(feature = "sha-3")] -pub mod sha3; +mod sha3; mod sign; +/// A trait for creating digests of an array of bytes pub trait DigestCreate { + /// The name of the digest algorithm const NAME: &'static str; + /// Compute the digest of the input bytes fn compute(&mut self, input: &[u8]) -> String; } +/// A trait for verifying digests pub trait DigestVerify { + /// Verify the payload of the request against a slice of digests + /// + /// The slice of digests should never be empty fn verify(&mut self, digests: &[DigestPart], payload: &[u8]) -> bool; } +/// Extend the Sign trait with support for adding Digest Headers to the request +/// +/// It generates HTTP Signatures after the Digest header has been added, in order to have +/// verification that the body has not been tampered with, or that the request can't be replayed by +/// a malicious entity pub trait SignExt: Sign { + /// Set the Digest and Authorization headers on the request fn authorization_signature_with_digest( self, config: &Config, @@ -44,6 +62,7 @@ pub trait SignExt: Sign { V: AsRef<[u8]>, Self: Sized; + /// Set the Digest and Signature headers on the request fn signature_with_digest( self, config: &Config, @@ -61,11 +80,19 @@ pub trait SignExt: Sign { Self: Sized; } +/// A parsed digest from the request pub struct DigestPart { + /// The alrogithm used to produce the digest pub algorithm: String, + + /// The digest itself pub digest: String, } +/// An intermediate type between setting the Digest and Signature or Authorization headers, and +/// actually sending the request +/// +/// This exists so that the return type for the [`SignExt`] trait can be named pub struct DigestClient { req: ClientRequest, body: V, @@ -75,10 +102,14 @@ impl DigestClient where V: AsRef<[u8]>, { - pub fn new(req: ClientRequest, body: V) -> Self { + fn new(req: ClientRequest, body: V) -> Self { DigestClient { req, body } } + /// Send the request + /// + /// This is analogous to `ClientRequest::send_body` and uses the body provided when producing + /// the digest pub fn send( self, ) -> impl Future>> { diff --git a/http-signature-normalization-actix/src/lib.rs b/http-signature-normalization-actix/src/lib.rs index a4ab017..72f8caf 100644 --- a/http-signature-normalization-actix/src/lib.rs +++ b/http-signature-normalization-actix/src/lib.rs @@ -1,8 +1,176 @@ +#![deny(missing_docs)] + +//! # Integration of Http Signature Normalization with Actix Web +//! +//! This library provides middlewares for verifying HTTP Signature headers and, optionally, Digest +//! headers with the `digest` feature enabled. It also extends actix_web's ClientRequest type to +//! add signatures and digests to the request +//! +//! ### Use it in a server +//! ```rust,ignore +//! use actix::System; +//! use actix_web::{web, App, HttpResponse, HttpServer, ResponseError}; +//! use failure::Fail; +//! use http_signature_normalization_actix::{prelude::*, verify::Algorithm}; +//! use sha2::{Digest, Sha256}; +//! +//! #[derive(Clone, Debug)] +//! struct MyVerify; +//! +//! impl SignatureVerify for MyVerify { +//! type Error = MyError; +//! type Future = Result; +//! +//! fn signature_verify( +//! &mut self, +//! algorithm: Option, +//! key_id: &str, +//! signature: &str, +//! signing_string: &str, +//! ) -> Self::Future { +//! match algorithm { +//! Some(Algorithm::Hs2019) => (), +//! _ => return Err(MyError::Algorithm), +//! }; +//! +//! if key_id != "my-key-id" { +//! return Err(MyError::Key); +//! } +//! +//! let decoded = base64::decode(signature).map_err(|_| MyError::Decode)?; +//! +//! // In a real system, you'd want to actually verify a signature, not just check for +//! // byte equality +//! Ok(decoded == signing_string.as_bytes()) +//! } +//! } +//! +//! fn index(_: (DigestVerified, SignatureVerified)) -> &'static str { +//! "Eyyyyup" +//! } +//! +//! fn main() -> Result<(), Box> { +//! let sys = System::new("server-example"); +//! +//! let config = Config::default(); +//! +//! HttpServer::new(move || { +//! App::new() +//! .wrap(VerifyDigest::new(Sha256::new()).optional()) +//! .wrap( +//! VerifySignature::new(MyVerify, config.clone()) +//! .authorization() +//! .optional(), +//! ) +//! .route("/", web::post().to(index)) +//! }) +//! .bind("127.0.0.1:8010")? +//! .start(); +//! +//! sys.run()?; +//! Ok(()) +//! } +//! +//! #[derive(Debug, Fail)] +//! enum MyError { +//! #[fail(display = "Failed to verify, {}", _0)] +//! Verify(#[cause] PrepareVerifyError), +//! +//! #[fail(display = "Unsupported algorithm")] +//! Algorithm, +//! +//! #[fail(display = "Couldn't decode signature")] +//! Decode, +//! +//! #[fail(display = "Invalid key")] +//! Key, +//! } +//! +//! impl ResponseError for MyError { +//! fn error_response(&self) -> HttpResponse { +//! HttpResponse::BadRequest().finish() +//! } +//! +//! fn render_response(&self) -> HttpResponse { +//! self.error_response() +//! } +//! } +//! +//! impl From for MyError { +//! fn from(e: PrepareVerifyError) -> Self { +//! MyError::Verify(e) +//! } +//! } +//! ``` +//! +//! ### Use it in a client +//! ```rust,ignore +//! use actix::System; +//! use actix_web::client::Client; +//! use failure::Fail; +//! use futures::future::{lazy, Future}; +//! use http_signature_normalization_actix::prelude::*; +//! use sha2::{Digest, Sha256}; +//! +//! fn main() { +//! System::new("client-example") +//! .block_on(lazy(|| { +//! let config = Config::default(); +//! let mut digest = Sha256::new(); +//! +//! Client::default() +//! .post("http://127.0.0.1:8010/") +//! .header("User-Agent", "Actix Web") +//! .authorization_signature_with_digest( +//! &config, +//! "my-key-id", +//! &mut digest, +//! "Hewwo-owo", +//! |s| { +//! // In a real-world system, you'd actually want to sign the string, +//! // not just base64 encode it +//! Ok(base64::encode(s)) as Result<_, MyError> +//! }, +//! ) +//! .unwrap() +//! .send() +//! .map_err(|_| ()) +//! .and_then(|mut res| res.body().map_err(|_| ())) +//! .map(|body| { +//! println!("{:?}", body); +//! }) +//! })) +//! .unwrap(); +//! } +//! +//! #[derive(Debug, Fail)] +//! pub enum MyError { +//! #[fail(display = "Failed to read header, {}", _0)] +//! Convert(#[cause] ToStrError), +//! +//! #[fail(display = "Failed to create header, {}", _0)] +//! Header(#[cause] InvalidHeaderValue), +//! } +//! +//! impl From for MyError { +//! fn from(e: ToStrError) -> Self { +//! MyError::Convert(e) +//! } +//! } +//! +//! impl From for MyError { +//! fn from(e: InvalidHeaderValue) -> Self { +//! MyError::Header(e) +//! } +//! } +//! ``` + use actix_web::http::{ header::{HeaderMap, InvalidHeaderValue, ToStrError}, uri::PathAndQuery, Method, }; + use failure::Fail; use futures::future::IntoFuture; use std::{collections::BTreeMap, fmt::Display}; @@ -14,11 +182,13 @@ pub mod digest; pub mod create; pub mod middleware; + +/// Useful types and traits for using this library in Actix Web pub mod prelude { pub use crate::{ middleware::{SignatureVerified, VerifySignature}, verify::Unverified, - Config, Sign, SignatureVerify, VerifyError, + Config, PrepareVerifyError, Sign, SignatureVerify, }; #[cfg(feature = "digest")] @@ -29,6 +199,8 @@ pub mod prelude { pub use actix_web::http::header::{InvalidHeaderValue, ToStrError}; } + +/// Types for Verifying an HTTP Signature pub mod verify { pub use http_signature_normalization::verify::{ Algorithm, DeprecatedAlgorithm, ParseSignatureError, ParsedHeader, Unvalidated, Unverified, @@ -41,10 +213,17 @@ use self::{ verify::{Algorithm, Unverified}, }; +/// A trait for verifying signatures pub trait SignatureVerify { + /// An error produced while attempting to verify the signature. This can be anything + /// implementing ResponseError type Error: actix_web::ResponseError; + + /// The future that resolves to the verification state of the signature type Future: IntoFuture; + /// Given the algorithm, key_id, signature, and signing_string, produce a future that resulves + /// to a the verification status fn signature_verify( &mut self, algorithm: Option, @@ -54,7 +233,9 @@ pub trait SignatureVerify { ) -> Self::Future; } +/// A trait implemented by the Actix Web ClientRequest type to add an HTTP signature to the request pub trait Sign { + /// Add an Authorization Signature to the request fn authorization_signature(self, config: &Config, key_id: K, f: F) -> Result where F: FnOnce(&str) -> Result, @@ -62,6 +243,7 @@ pub trait Sign { K: Display, Self: Sized; + /// Add a Signature to the request fn signature(self, config: &Config, key_id: K, f: F) -> Result where F: FnOnce(&str) -> Result, @@ -71,20 +253,26 @@ pub trait Sign { } #[derive(Clone, Debug, Default)] +/// A thin wrapper around the underlying library's config type pub struct Config { + /// The inner config type pub config: http_signature_normalization::Config, } #[derive(Debug, Fail)] -pub enum VerifyError { +/// An error when preparing to verify a request +pub enum PrepareVerifyError { #[fail(display = "Signature error, {}", _0)] - Sig(#[cause] http_signature_normalization::VerifyError), + /// An error validating the request + Sig(#[cause] http_signature_normalization::PrepareVerifyError), #[fail(display = "Failed to read header, {}", _0)] + /// An error converting the header to a string for validation Header(#[cause] ToStrError), } impl Config { + /// Begin the process of singing a request pub fn begin_sign( &self, method: &Method, @@ -107,12 +295,13 @@ impl Config { Ok(Unsigned { unsigned }) } + /// Begin the proess of verifying a request pub fn begin_verify( &self, method: &Method, path_and_query: Option<&PathAndQuery>, headers: HeaderMap, - ) -> Result { + ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) @@ -130,14 +319,14 @@ impl Config { } } -impl From for VerifyError { - fn from(e: http_signature_normalization::VerifyError) -> Self { - VerifyError::Sig(e) +impl From for PrepareVerifyError { + fn from(e: http_signature_normalization::PrepareVerifyError) -> Self { + PrepareVerifyError::Sig(e) } } -impl From for VerifyError { +impl From for PrepareVerifyError { fn from(e: ToStrError) -> Self { - VerifyError::Header(e) + PrepareVerifyError::Header(e) } } diff --git a/http-signature-normalization-actix/src/middleware.rs b/http-signature-normalization-actix/src/middleware.rs index dfe1c86..7b27978 100644 --- a/http-signature-normalization-actix/src/middleware.rs +++ b/http-signature-normalization-actix/src/middleware.rs @@ -1,3 +1,5 @@ +//! Types for verifying requests with Actix Web + use actix_web::{ dev::{Body, Payload, Service, ServiceRequest, ServiceResponse, Transform}, FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, @@ -12,32 +14,65 @@ use std::{cell::RefCell, rc::Rc}; use crate::{Config, SignatureVerify}; #[derive(Copy, Clone, Debug)] +/// A marker type that can be used to guard routes when the signature middleware is set to +/// 'optional' pub struct SignatureVerified; + #[derive(Clone, Debug)] +/// The Verify signature middleware +/// +/// ```rust,ignore +/// let middleware = VerifySignature::new(MyVerifier::new(), Config::default()) +/// .authorization() +/// .optional(); +/// +/// HttpServer::new(move || { +/// App::new() +/// .wrap(middleware.clone()) +/// .route("/protected", web::post().to(|_: SignatureVerified| "Verified Authorization Header")) +/// .route("/unprotected", web::post().to(|| "No verification required")) +/// }) +/// ``` pub struct VerifySignature(T, Config, HeaderKind, bool); + #[derive(Clone, Debug)] +#[doc(hidden)] pub struct VerifyMiddleware(Rc>, Config, HeaderKind, bool, T); + #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum HeaderKind { +enum HeaderKind { Authorization, Signature, } + #[derive(Clone, Debug, Fail)] #[fail(display = "Failed to verify http signature")] +#[doc(hidden)] pub struct VerifyError; impl VerifySignature where T: SignatureVerify, { + /// Create a new middleware for verifying HTTP Signatures. A type implementing + /// [`SignatureVerify`] is required, as well as a Config + /// + /// By default, this middleware expects to verify Signature headers, and requires the presence + /// of the header pub fn new(verify_signature: T, config: Config) -> Self { VerifySignature(verify_signature, config, HeaderKind::Signature, true) } + /// Verify Authorization headers instead of Signature headers pub fn authorization(self) -> Self { VerifySignature(self.0, self.1, HeaderKind::Authorization, self.3) } + /// Mark the presence of a Signature or Authorization header as optional + /// + /// If a header is present, it will be verified, but if there is not one present, the request + /// is passed through. This can be used to set a global middleware, and then guard each route + /// handler with the [`SignatureVerified`] type. pub fn optional(self) -> Self { VerifySignature(self.0, self.1, self.2, false) } diff --git a/http-signature-normalization-http/src/create.rs b/http-signature-normalization-http/src/create.rs index 1496d96..dee73a9 100644 --- a/http-signature-normalization-http/src/create.rs +++ b/http-signature-normalization-http/src/create.rs @@ -1,14 +1,22 @@ +//! Types used for signing a request use http::header::{HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue, AUTHORIZATION}; +/// A thin wrapper around the base library's Signed type pub struct Signed { + /// The inner Signed type which can produce string versions of HTTP headers pub signed: http_signature_normalization::create::Signed, } +/// A thin wrapper around the base library's Unsigned type +/// +/// This is used to produce a Signed type that can interact with http's types pub struct Unsigned { + /// The inner Unsigned type, which can produce the base library's Signed type pub unsigned: http_signature_normalization::create::Unsigned, } impl Signed { + /// Set the Signature Header in a given HeaderMap pub fn signature_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { hm.insert( HeaderName::from_static("Signature"), @@ -18,6 +26,7 @@ impl Signed { Ok(()) } + /// Set the Authorization Header in a given HeaderMap pub fn authorization_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { hm.insert( AUTHORIZATION, @@ -28,9 +37,17 @@ impl Signed { } impl Unsigned { + /// Sign the request + /// + /// ```rust,ignore + /// let signed = unsigned.sign("my-key-id".to_owned(), |signing_string| { + /// let signature = private_key.sign(signing_string)?; + /// Ok(base64::encode(signature)) + /// })?; + /// ``` pub fn sign(self, key_id: String, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, { let signed = self.unsigned.sign(key_id, f)?; Ok(Signed { signed }) diff --git a/http-signature-normalization-http/src/lib.rs b/http-signature-normalization-http/src/lib.rs index 9f71c67..eaea6ae 100644 --- a/http-signature-normalization-http/src/lib.rs +++ b/http-signature-normalization-http/src/lib.rs @@ -1,3 +1,9 @@ +#![deny(missing_docs)] +//! Integration of Http Signature Normalization with the HTTP crate +//! +//! This provides a thin wrapper around transforming HTTP's `HeaderMap`, `PathAndQuery`, and +//! `Method` types into BTreeMaps and Strings for signing and verifying requests + use http::{ header::{HeaderMap, ToStrError}, method::Method, @@ -7,6 +13,7 @@ use std::{collections::BTreeMap, error::Error, fmt}; use self::{create::Unsigned, verify::Unverified}; +/// Export useful types signing and verifying requests pub mod prelude { pub use http::{ header::{HeaderMap, InvalidHeaderValue, ToStrError}, @@ -21,11 +28,12 @@ pub mod prelude { ValidateError, }; - pub use crate::{Config, VerifyError}; + pub use crate::{Config, PrepareVerifyError}; } pub mod create; +/// Export types used for signature verification pub mod verify { pub use http_signature_normalization::verify::{ Algorithm, DeprecatedAlgorithm, ParseSignatureError, ParsedHeader, Unvalidated, Unverified, @@ -34,17 +42,25 @@ pub mod verify { } #[derive(Clone, Default)] +/// Thinly wrap Http Signature Normalization's config type pub struct Config { + /// Expose the inner Config pub config: http_signature_normalization::Config, } #[derive(Debug)] -pub enum VerifyError { - Sig(http_signature_normalization::VerifyError), +/// Errors produced when preparing to verify an Http Signature +pub enum PrepareVerifyError { + /// There was an error in the underlying library + Sig(http_signature_normalization::PrepareVerifyError), + /// There was an error producing a String from the HeaderValue Header(ToStrError), } impl Config { + /// Begin the process of signing a request + /// + /// The types required from this function can be produced from http's Request and URI types. pub fn begin_sign( &self, method: &Method, @@ -67,12 +83,15 @@ impl Config { Ok(Unsigned { unsigned }) } + /// Begin the process of verifying a request + /// + /// The types required from this function can be produced from http's Request and URI types. pub fn begin_verify( &self, method: &Method, path_and_query: Option<&PathAndQuery>, headers: HeaderMap, - ) -> Result { + ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) @@ -90,39 +109,39 @@ impl Config { } } -impl fmt::Display for VerifyError { +impl fmt::Display for PrepareVerifyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - VerifyError::Sig(ref e) => write!(f, "Sig error, {}", e), - VerifyError::Header(ref e) => write!(f, "Header error, {}", e), + PrepareVerifyError::Sig(ref e) => write!(f, "Sig error, {}", e), + PrepareVerifyError::Header(ref e) => write!(f, "Header error, {}", e), } } } -impl Error for VerifyError { +impl Error for PrepareVerifyError { fn description(&self) -> &str { match *self { - VerifyError::Sig(ref e) => e.description(), - VerifyError::Header(ref e) => e.description(), + PrepareVerifyError::Sig(ref e) => e.description(), + PrepareVerifyError::Header(ref e) => e.description(), } } fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { - VerifyError::Sig(ref e) => Some(e), - VerifyError::Header(ref e) => Some(e), + PrepareVerifyError::Sig(ref e) => Some(e), + PrepareVerifyError::Header(ref e) => Some(e), } } } -impl From for VerifyError { - fn from(e: http_signature_normalization::VerifyError) -> Self { - VerifyError::Sig(e) +impl From for PrepareVerifyError { + fn from(e: http_signature_normalization::PrepareVerifyError) -> Self { + PrepareVerifyError::Sig(e) } } -impl From for VerifyError { +impl From for PrepareVerifyError { fn from(e: ToStrError) -> Self { - VerifyError::Header(e) + PrepareVerifyError::Header(e) } } diff --git a/src/create.rs b/src/create.rs index 35931b0..0ab8edf 100644 --- a/src/create.rs +++ b/src/create.rs @@ -1,3 +1,4 @@ +//! Types and logic for creating signature and authorization headers use chrono::{DateTime, Utc}; use crate::{ @@ -6,6 +7,9 @@ use crate::{ }; #[derive(Debug)] +/// The signed stage of creating a signature +/// +/// From here, the Signature or Authorization headers can be generated as string pub struct Signed { signature: String, sig_headers: Vec, @@ -15,6 +19,10 @@ pub struct Signed { } #[derive(Debug)] +/// The Unsigned stage of creating a signature +/// +/// From here, the `sign` method can be used to sign the signing_string, producing a [`Signed`] +/// type. pub struct Unsigned { pub(crate) signing_string: String, pub(crate) sig_headers: Vec, @@ -23,10 +31,16 @@ pub struct Unsigned { } impl Signed { + /// Turn the Signed type into a String that can be used as the Signature Header + /// + /// Done manually, it would look like `format!("Signature: {}", signed.signature_header())` pub fn signature_header(self) -> String { format!("Signature {}", self.into_header()) } + /// Turn the Signed type into a String that can be used as the Authorization Header + /// + /// Done manually, it would look like `format!("Authorization: {}", signed.authorization_header())` pub fn authorization_header(self) -> String { self.into_header() } @@ -50,6 +64,10 @@ impl Signed { } impl Unsigned { + /// Sign the signing string, producing a String that can be used in an HTTP Header + /// + /// When using RSA or HMAC to sign the string, be sure to base64-encode the result to produce a + /// String. pub fn sign(self, key_id: String, f: F) -> Result where F: FnOnce(&str) -> Result, diff --git a/src/lib.rs b/src/lib.rs index cf619e1..2e1b044 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,49 @@ +#![deny(missing_docs)] + +//! # HTTP Signature Normaliztion +//! _An HTTP Signatures library that leaves the signing to you_ +//! +//! - [crates.io](https://crates.io/crates/http-signature-normalization) +//! - [docs.rs](https://docs.rs/http-signature-normalization) +//! - [Join the discussion on Matrix](https://matrix.to/#/!IRQaBCMWKbpBWKjQgx:asonix.dog?via=asonix.dog) +//! +//! Http Signature Normalization is a minimal-dependency crate for producing HTTP Signatures with user-provided signing and verification. The API is simple; there's a series of steps for creation and verification with types that ensure reasonable usage. +//! +//! ```rust +//! use chrono::Duration; +//! use http_signature_normalization::Config; +//! use std::collections::BTreeMap; +//! +//! fn main() -> Result<(), Box> { +//! let config = Config { +//! expires_after: Duration::seconds(5), +//! }; +//! +//! let headers = BTreeMap::new(); +//! +//! let signature_header_value = config +//! .begin_sign("GET", "/foo?bar=baz", headers) +//! .sign("my-key-id".to_owned(), |signing_string| { +//! // sign the string here +//! Ok(signing_string.to_owned()) as Result<_, Box> +//! })? +//! .signature_header(); +//! +//! let mut headers = BTreeMap::new(); +//! headers.insert("Signature".to_owned(), signature_header_value); +//! +//! let verified = config +//! .begin_verify("GET", "/foo?bar=baz", headers)? +//! .verify(|sig, signing_string| { +//! // Verify the signature here +//! sig == signing_string +//! }); +//! +//! assert!(verified); +//! Ok(()) +//! } +//! ``` + use chrono::{DateTime, Duration, Utc}; use std::{collections::BTreeMap, error::Error, fmt}; @@ -22,17 +68,29 @@ const HEADERS_FIELD: &'static str = "headers"; const SIGNATURE_FIELD: &'static str = "signature"; #[derive(Clone, Debug)] +/// Configuration for signing and verifying signatures +/// +/// Currently, the only configuration provided is how long a signature should be considered valid +/// before it expires. pub struct Config { + /// How long a singature is valid pub expires_after: Duration, } #[derive(Debug)] -pub enum VerifyError { +/// Error preparing a header for validation +/// +/// This could be due to a missing header, and unparsable header, or an expired header +pub enum PrepareVerifyError { + /// Error validating the header Validate(ValidateError), + /// Error parsing the header Parse(ParseSignatureError), } impl Config { + /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to + /// sign the header pub fn begin_sign( &self, method: &str, @@ -65,12 +123,14 @@ impl Config { } } + /// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to + /// verify the header pub fn begin_verify( &self, method: &str, path_and_query: &str, headers: BTreeMap, - ) -> Result { + ) -> Result { let mut headers: BTreeMap = headers .into_iter() .map(|(k, v)| (k.to_lowercase().to_owned(), v)) @@ -129,40 +189,40 @@ fn build_signing_string( signing_string } -impl fmt::Display for VerifyError { +impl fmt::Display for PrepareVerifyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - VerifyError::Validate(ref e) => fmt::Display::fmt(e, f), - VerifyError::Parse(ref e) => fmt::Display::fmt(e, f), + PrepareVerifyError::Validate(ref e) => fmt::Display::fmt(e, f), + PrepareVerifyError::Parse(ref e) => fmt::Display::fmt(e, f), } } } -impl Error for VerifyError { +impl Error for PrepareVerifyError { fn description(&self) -> &str { match *self { - VerifyError::Validate(ref e) => e.description(), - VerifyError::Parse(ref e) => e.description(), + PrepareVerifyError::Validate(ref e) => e.description(), + PrepareVerifyError::Parse(ref e) => e.description(), } } fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { - VerifyError::Validate(ref e) => Some(e), - VerifyError::Parse(ref e) => Some(e), + PrepareVerifyError::Validate(ref e) => Some(e), + PrepareVerifyError::Parse(ref e) => Some(e), } } } -impl From for VerifyError { +impl From for PrepareVerifyError { fn from(v: ValidateError) -> Self { - VerifyError::Validate(v) + PrepareVerifyError::Validate(v) } } -impl From for VerifyError { +impl From for PrepareVerifyError { fn from(p: ParseSignatureError) -> Self { - VerifyError::Parse(p) + PrepareVerifyError::Parse(p) } } @@ -189,7 +249,7 @@ mod tests { } #[test] - fn round_trip() { + fn round_trip_authorization() { let headers = prepare_headers(); let config = Config::default(); @@ -211,4 +271,28 @@ mod tests { assert!(verified); } + + #[test] + fn round_trip_signature() { + let headers = prepare_headers(); + let config = Config::default(); + + let signature_header = config + .begin_sign("GET", "/foo?bar=baz", headers) + .sign("hi".to_owned(), |s| { + Ok(s.to_owned()) as Result<_, std::io::Error> + }) + .unwrap() + .signature_header(); + + let mut headers = prepare_headers(); + headers.insert("Signature".to_owned(), signature_header); + + let verified = config + .begin_verify("GET", "/foo?bar=baz", headers) + .unwrap() + .verify(|sig, signing_string| sig == signing_string); + + assert!(verified); + } } diff --git a/src/verify.rs b/src/verify.rs index 91b2c1f..839a402 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,3 +1,4 @@ +//! Types and methods to verify a signature or authorization header use chrono::{DateTime, Duration, TimeZone, Utc}; use std::{ collections::{BTreeMap, HashMap}, @@ -12,6 +13,10 @@ use crate::{ }; #[derive(Debug)] +/// The Unverified step of the verification process +/// +/// This type is the result of performing some basic validation on the parsed header, and can be +/// used to verify the header pub struct Unverified { key_id: String, signature: String, @@ -20,6 +25,10 @@ pub struct Unverified { } #[derive(Debug)] +/// The Unvalidated stage +/// +/// This is created after generating the signing string from a parsed header, and transitions into +/// the Unverified type by applying some basic validations pub struct Unvalidated { pub(crate) key_id: String, pub(crate) signature: String, @@ -31,6 +40,7 @@ pub struct Unvalidated { } #[derive(Debug)] +/// The successful result of parsing a Signature or Authorization header pub struct ParsedHeader { signature: String, key_id: String, @@ -42,46 +52,102 @@ pub struct ParsedHeader { } #[derive(Clone, Copy, Debug)] +/// Algorithms that may be present in an HTTP Signature's `algorithm` field, but are considered +/// deprecated due to security issues +/// +/// Most of these are Deprecated solely because the presence of the algorithm's name in the request +/// could be used to gain insight into ways to forge requests. This doesn't mean that using these +/// algorithms to sign and verify requests is bad, it just means that stating which algorithm is in +/// use is dangerous. In the case of the SHA1 variants, they were deprecated for being weak +/// hashes. +/// +/// This library only produces HTTP Signatures with the "HS2019" algorithm type, and leaves +/// deciding which algorithm to actually use to implementors pub enum DeprecatedAlgorithm { + /// HMAC SHA-1 HmacSha1, + /// HMAC SHA-256 HmacSha256, + /// HMAC SHA-384 HmacSha384, + /// HMAC SHA-512 HmacSha512, + /// RSA SHA-1 RsaSha1, + /// RSA SHA-256 RsaSha256, + /// RSA SHA-384 RsaSha384, + /// RSA SHA-512 RsaSha512, + /// ECDSA SHA-1 EcdsaSha1, + /// ECDSA SHA-256 EcdsaSha256, + /// ECDSA SHA-384 EcdsaSha384, + /// ECDSA SHA-512 EcdsaSha512, } #[derive(Clone, Debug)] +/// Kinds of algorithms +/// +/// This library knows about HS2019 as a supported algorithm, and any other algorithms are either +/// unknown at the time of writing, or deprecated pub enum Algorithm { + /// The only officially supported algorithm from the current HTTP Signatures specification Hs2019, + /// Algorithms that have been used historically, but are deprecated Deprecated(DeprecatedAlgorithm), + /// Algorithms that may be used by custom implementations and are unknown to the spec Unknown(String), } #[derive(Clone, Debug)] +/// Kinds of errors for validating a request pub enum ValidateError { + /// The Authorization or Signature header is not present Missing, + /// The request's `created` or `expires` field indicates it is too old to be valid Expired, } #[derive(Clone, Debug)] +/// The error produced when parsing the HTTPT Signature fails, including the name of the field that +/// was invalid. pub struct ParseSignatureError(&'static str); impl Unverified { + /// Get the Key ID from an Unverified type + /// + /// This is useful for looking up the proper verification key to verify the request pub fn key_id(&self) -> &str { &self.key_id } + /// Get the Algorithm used in the request, if one is present + /// + /// If the algorithm is present and is not what an implementor expected, they should not + /// attempt to verify the signature pub fn algorithm(&self) -> Option<&Algorithm> { self.algorithm.as_ref() } + /// Verify the signature with the signature and the signing string + /// + /// ```rust,ignore + /// unverified.verify(|signature, signing_string| { + /// let bytes = match base64::decode(signature) { + /// Ok(bytes) => bytes, + /// Err(_) => return false, + /// }; + /// + /// public_key + /// .verify(bytes, signing_string) + /// .unwrap_or(false) + /// }) + /// ``` pub fn verify(&self, f: F) -> T where F: FnOnce(&str, &str) -> T, @@ -91,6 +157,8 @@ impl Unverified { } impl Unvalidated { + /// Validate parts of the header, ensuring that the provided dates don't indicate that it is + /// expired. pub fn validate(self, expires_after: Duration) -> Result { if let Some(expires) = self.expires { if expires < self.parsed_at { @@ -113,6 +181,7 @@ impl Unvalidated { } impl ParsedHeader { + /// Generate a Signing String from the header pub fn into_unvalidated( self, method: &str,