Prepare for release

This commit is contained in:
asonix 2019-09-21 11:26:11 -05:00
parent 6f0ff4ca5f
commit 4b2b63143c
15 changed files with 1227 additions and 52 deletions

View file

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>>
})?
.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.

View file

@ -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 <asonix@asonix.dog>"]
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"]

View file

@ -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.

View file

@ -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<ToStrError> for MyError {
fn from(e: ToStrError) -> Self {
MyError::Convert(e)
}
}
impl From<InvalidHeaderValue> 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<bool, Self::Error>;
fn signature_verify(
&mut self,
algorithm: Option<Algorithm>,
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<dyn std::error::Error>> {
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<PrepareVerifyError> 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.

View file

@ -62,7 +62,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[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<VerifyError> for MyError {
fn from(e: VerifyError) -> Self {
impl From<PrepareVerifyError> for MyError {
fn from(e: PrepareVerifyError) -> Self {
MyError::Verify(e)
}
}

View file

@ -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<F, E>(self, key_id: String, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<String, E>,

View file

@ -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<T>(bool, T);
#[doc(hidden)]
pub struct VerifyMiddleware<T, S>(Rc<RefCell<S>>, bool, T);
#[derive(Debug, Fail)]
#[fail(display = "Error verifying digest")]
#[doc(hidden)]
pub struct VerifyError;
impl<T> VerifyDigest<T>
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)
}

View file

@ -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<F, E, K, D, V>(
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<F, E, K, D, V>(
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<V> {
req: ClientRequest,
body: V,
@ -75,10 +102,14 @@ impl<V> DigestClient<V>
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<Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>> {

View file

@ -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<bool, Self::Error>;
//!
//! fn signature_verify(
//! &mut self,
//! algorithm: Option<Algorithm>,
//! 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<dyn std::error::Error>> {
//! 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<PrepareVerifyError> 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<ToStrError> for MyError {
//! fn from(e: ToStrError) -> Self {
//! MyError::Convert(e)
//! }
//! }
//!
//! impl From<InvalidHeaderValue> 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<Item = bool, Error = Self::Error>;
/// 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<Algorithm>,
@ -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<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<String, E>,
@ -62,6 +243,7 @@ pub trait Sign {
K: Display,
Self: Sized;
/// Add a Signature to the request
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<String, E>,
@ -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<Unverified, VerifyError> {
) -> Result<Unverified, PrepareVerifyError> {
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<http_signature_normalization::VerifyError> for VerifyError {
fn from(e: http_signature_normalization::VerifyError) -> Self {
VerifyError::Sig(e)
impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
fn from(e: http_signature_normalization::PrepareVerifyError) -> Self {
PrepareVerifyError::Sig(e)
}
}
impl From<ToStrError> for VerifyError {
impl From<ToStrError> for PrepareVerifyError {
fn from(e: ToStrError) -> Self {
VerifyError::Header(e)
PrepareVerifyError::Header(e)
}
}

View file

@ -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>(T, Config, HeaderKind, bool);
#[derive(Clone, Debug)]
#[doc(hidden)]
pub struct VerifyMiddleware<T, S>(Rc<RefCell<S>>, 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<T> VerifySignature<T>
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)
}

View file

@ -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<F, E>(self, key_id: String, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
{
let signed = self.unsigned.sign(key_id, f)?;
Ok(Signed { signed })

View file

@ -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<Unverified, VerifyError> {
) -> Result<Unverified, PrepareVerifyError> {
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<http_signature_normalization::VerifyError> for VerifyError {
fn from(e: http_signature_normalization::VerifyError) -> Self {
VerifyError::Sig(e)
impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
fn from(e: http_signature_normalization::PrepareVerifyError) -> Self {
PrepareVerifyError::Sig(e)
}
}
impl From<ToStrError> for VerifyError {
impl From<ToStrError> for PrepareVerifyError {
fn from(e: ToStrError) -> Self {
VerifyError::Header(e)
PrepareVerifyError::Header(e)
}
}

View file

@ -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<String>,
@ -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<String>,
@ -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<F, E>(self, key_id: String, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<String, E>,

View file

@ -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<dyn std::error::Error>> {
//! 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<dyn std::error::Error>>
//! })?
//! .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<String, String>,
) -> Result<Unverified, VerifyError> {
) -> Result<Unverified, PrepareVerifyError> {
let mut headers: BTreeMap<String, String> = 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<ValidateError> for VerifyError {
impl From<ValidateError> for PrepareVerifyError {
fn from(v: ValidateError) -> Self {
VerifyError::Validate(v)
PrepareVerifyError::Validate(v)
}
}
impl From<ParseSignatureError> for VerifyError {
impl From<ParseSignatureError> 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);
}
}

View file

@ -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<F, T>(&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<Unverified, ValidateError> {
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,