15 releases (7 breaking)

0.8.0 Jan 18, 2019
0.6.0 Dec 22, 2018
0.5.2 Nov 21, 2018
0.1.3 Jan 15, 2018
0.1.2 Dec 24, 2017

#1355 in HTTP server

GPL-3.0 license

88KB
1.5K SLoC

HTTP Signatures

This crate is used to create and verify HTTP Signatures, defined here. It has support for Actix Web, Hyper, Rocket, and Reqwest types. In the future, I might also support Iron middleware for verification.

crates.io documentation

Running the examples

Each example is hosted within it's own crate in the examples folder

The hyper examples are configured to talk to eachother by default. The server runs on port 3000, and the client POSTs on port 3000. They also use the Signature header to sign and verify the request.

The actix-web server and client examples listens and post to port 5000, and can be tested with the each other. They use the Signature header to sign and verify the request.

The rocket 4 server listen on port 8000, and the reqwest client can be used to interact with it. These examples use the Authorization header to sign and verify the request.

Usage

With Actix Web

Add this to your Cargo.toml

[dependencies.http-signatures]
version = "0.7"
features = ["use_actix_web"]
Server Usage

Use it in your application like this.

const PHRASE: &str = "Hewwo, Mr. Obama???";

#[derive(Debug, Fail)]
#[fail(display = "Error verifying signature, {}", _0)]
pub struct Error(#[cause] http_signatures::Error);

impl ResponseError for Error {}

#[derive(Clone)]
pub struct MyState {
    arc: Arc<Vec<u8>>,
}

fn index((req, state): (HttpRequest<MyState>, State<MyState>)) -> impl Responder {
    let verified = req.verify_signature_header(VerifyKey::unchecked_from_slice(&state.arc));

    verified.map_err(|e| Error(e.into())).map(|_| {
        HttpResponse::Ok()
            .content_length(PHRASE.len() as u64)
            .content_type("text/plain")
            .force_close()
            .body(format!("{}", PHRASE))
    })
}

fn main() -> Result<(), Box<dyn Fail> {
    let mut key_file = File::open("../../tests/assets/public.der")?;
    let mut key_file_vec = Vec::new();
    key_file.read_to_end(&mut key_file_vec)?;
    let key_arc = Arc::new(key_file_vec);

    let state = MyState { arc: key_arc };

    server::new(move || App::with_state(state.clone()).resource("/", |r| r.with(index)))
        .bind("127.0.0.1:5000")
        .expect("Can not bind to port 5000")
        .run();

    Ok(())
}
Client Usage
let key_id = "some-username-or-something";
let json = r#"{"library":"actix-web"}"#;

let mut req = post("http://localhost:5000")
    .content_length(json.len() as u64)
    .content_type("application/json")
    .body(json)?;

req.with_signature_header(
    key_id.to_owned(),
    CreateKey::rsa(private_key, ShaSize::SHA256),
)?;

actix_web::actix::run(move || {
    req.send().map_err(|_| ()).map(|res| {
        println!("POST: {}", res.status());
        actix_web::actix::System::current().stop();
    })
});

With Hyper

Add this to your Cargo.toml

[dependencies.http-signatures]
version = "0.7"
features = ["use_hyper"]
Client

Use it when building a request as follows.

let client = Client::new();

let json = r#"{"library":"hyper"}"#;
let mut req = Request::post("http://localhost:3000")
    .body(Body::from(json))?;
req.headers_mut().insert(
    CONTENT_TYPE,
    HeaderValue::from_str("application/json")?,
);
req.headers_mut().insert(
    CONTENT_LENGTH,
    HeaderValue::from_str(&format!("{}", json.len()))?,
);

let key_id = "some-username-or-something";
// Add the HTTP Signature
req.with_signature_header(
    key_id.into(),
    CreateKey::rsa(private_key, ShaSize::SHA256),
)?;

let post = client.request(req).and_then(|res| {
    println!("POST: {}", res.status());

    res.into_body().concat2()
});

tokio::run(post.map(|_| ()).map_err(|_| ()));
Server

This is a very basic example server outline that should give you a general idea of how to set up a Hyper server that verifies HTTP Signatures. This is not meant to be code that actually works.

const PHRASE: &str = "Hewwo, Mr. Obama???";

fn main() -> Result<(), Box<dyn Error> {
    let mut key_file = File::open("../../tests/assets/public.der")?;
    let mut key_file_vec = Vec::new();
    key_file.read_to_end(&mut key_file_vec)?;
    let key_arc = Arc::new(key_file_vec);

    let service = move || {
        let key = Arc::clone(&key_arc);

        service_fn(move |req: Request<Body>| {
            let verified = req.verify_signature_header(VerifyKey::unchecked_from_slice(&key));

            verified
                .into_future()
                .map_err(|e| format!("{:?}", e))
                .and_then(|_| {
                    println!("Succesfully verified request!");
                    Response::builder()
                        .header(CONTENT_LENGTH, PHRASE.len() as u64)
                        .body(Body::from(PHRASE))
                        .map_err(|e| format!("{:?}", e))
                })
        })
    };

    let addr = "127.0.0.1:3000".parse()?;
    let server = Server::bind(&addr)
        .serve(service)
        .map_err(|e| eprintln!("server error: {}", e));
    rt::run(server);

    Ok(())
}

With Reqwest

Add this to your Cargo.toml

[dependencies.http-signatures]
version = "0.7"
default-features = false
features = ["use_reqwest"]

In your code, use it when building a request as follows.

let key_id = "some-username-or-something".into();

let client = Client::new();
let mut req = client.get("http://localhost:8000").build()?;

req.with_authorization_header(key_id, CreateKey::rsa(private_key, ShaSize::SHA512))?;

let res = client.execute(req)?;

With Rocket

Add this to your Cargo.toml

[dependencies.http-signatures]
version = "0.7"
default-features = false
features = ["use_rocket"]

In your code, use it in a route like so

struct Verified;

impl<'a, 'r> FromRequest<'a, 'r> for Verified {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Verified, ()> {
        let res = request
            .guard::<State<Vec<u8>>>()
            .succeeded()
            .ok_or(())
            .and_then(|key| {
                request
                    .verify_authorization_header(VerifyKey::unchecked_from_slice(&key))
                    .map_err(|e| println!("Error: {:?}", e))?;

                Ok(Verified)
            });

        match res {
            Ok(verified) => Success(verified),
            Err(fail) => Failure((Status::Forbidden, fail)),
        }
    }
}

#[get("/")]
fn index(_verified: Verified) -> &'static str {
    "Successfully verified request"
}

fn main() -> Result<(), Box<dyn Error> {
    let mut key_file = File::open("../../tests/assets/public.der")?;
    let mut key_vec = Vec::new();
    key_file.read_to_end(&mut key_vec)?;

    rocket::ignite()
        .mount("/", routes![index])
        .manage(key_vec)
        .launch();

    Ok(())
}

Contributing

Please be aware that all code contributed to this project will be licensed under the GPL version 3.

License

HTTP Signatures is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

HTTP Signatures is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This file is part of HTTP Signatures

You should have received a copy of the GNU General Public License along with HTTP Signatures If not, see http://www.gnu.org/licenses/.

Dependencies

~6–17MB
~336K SLoC