mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-14 05:31:15 +00:00
178 lines
5.3 KiB
Markdown
178 lines
5.3 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)
|
|
- [Hit me up on Mastodon](https://asonix.dog/@asonix)
|
|
|
|
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.10.0-alpha.1"
|
|
actix-web = "3.0.0-alpha.1"
|
|
thiserror = "0.1"
|
|
http-signature-normalization-actix = { version = "0.3.0-alpha.6", default-features = false, features = ["sha-2"] }
|
|
sha2 = "0.8"
|
|
```
|
|
|
|
#### Then, use it in your client
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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](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.
|