mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-25 10:51:01 +00:00
191 lines
5.6 KiB
Markdown
191 lines
5.6 KiB
Markdown
# 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.
|