#http #signatures #digest #actix

http-signature-normalization-actix

An HTTP Signatures library that leaves the signing to you

10 releases

✓ Uses Rust 2018 edition

new 0.3.0-alpha.8 Mar 25, 2020
0.3.0-alpha.7 Mar 20, 2020
0.2.0 Jan 18, 2020
0.1.0 Sep 21, 2019

#101 in Cryptography

Download history 1/week @ 2019-12-02 9/week @ 2019-12-09 1/week @ 2019-12-23 27/week @ 2020-01-13 12/week @ 2020-01-20 3/week @ 2020-02-03 2/week @ 2020-02-10 13/week @ 2020-02-17 8/week @ 2020-02-24 4/week @ 2020-03-09 422/week @ 2020-03-16

188 downloads per month

Custom license and MPL-2.0 licenses

76KB
1.5K SLoC

Rust 1K SLoC // 0.2% comments Razor 552 SLoC // 0.2% comments

HTTP Signature Normaliztion Actix

An HTTP Signatures library that leaves the signing to you

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

actix = "0.10.0-alpha.1"
actix-web = "3.0.0-alpha.1"
thiserror = "0.1"
http-signature-normalization-actix = { version = "0.3.0-alpha.8", default-features = false, features = ["sha-2"] }
sha2 = "0.8"

Then, use it in your client

use actix_web::client::Client;
use http_signature_normalization_actix::prelude::*;
use sha2::{Digest, Sha256};

#[actix_rt::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::default();
    let mut digest = Sha256::new();

    let mut response = 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>
        })?
        .send()
        .await
        .map_err(|e| {
            eprintln!("Error, {}", e);
            MyError::SendRequest
        })?;

    let body = response.body().await.map_err(|e| {
        eprintln!("Error, {}", e);
        MyError::Body
    })?;

    println!("{:?}", body);
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("Failed to read header, {0}")]
    Convert(#[from] ToStrError),

    #[error("Failed to create header, {0}")]
    Header(#[from] InvalidHeaderValue),

    #[error("Failed to send request")]
    SendRequest,

    #[error("Failed to retrieve request body")]
    Body,
}

Or, use it in your server

use actix_web::{http::StatusCode, web, App, HttpResponse, HttpServer, ResponseError};
use futures::future::{err, ok, Ready};
use http_signature_normalization_actix::prelude::*;
use sha2::{Digest, Sha256};

#[derive(Clone, Debug)]
struct MyVerify;

impl SignatureVerify for MyVerify {
    type Error = MyError;
    type Future = Ready<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 = match base64::decode(signature) {
            Ok(decoded) => decoded,
            Err(_) => return err(MyError::Decode),
        };

        ok(decoded == signing_string.as_bytes())
    }
}

async fn index((_, sig_verified): (DigestVerified, SignatureVerified)) -> &'static str {
    println!("Signature verified for {}", sig_verified.key_id());
    "Eyyyyup"
}

#[actix_rt::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    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")?
    .run()
    .await?;

    Ok(())
}

#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("Failed to verify, {}", _0)]
    Verify(#[from] PrepareVerifyError),

    #[error("Unsupported algorithm")]
    Algorithm,

    #[error("Couldn't decode signature")]
    Decode,

    #[error("Invalid key")]
    Key,
}

impl ResponseError for MyError {
    fn status_code(&self) -> StatusCode {
        StatusCode::BAD_REQUEST
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::BadRequest().finish()
    }
}

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

Dependencies

~18MB
~427K SLoC