# ERC-8128
**Signed HTTP Requests with Ethereum**
[![CI][ci-badge]][ci-url]
[![crates.io][crate-badge]][crate-url]
[![docs.rs][doc-badge]][doc-url]
[![License][license-badge]][license-url]
[![Rust][rust-badge]][rust-url]
[ci-badge]: https://github.com/qntx/erc8128/actions/workflows/rust.yml/badge.svg
[ci-url]: https://github.com/qntx/erc8128/actions/workflows/rust.yml
[crate-badge]: https://img.shields.io/crates/v/erc8128.svg
[crate-url]: https://crates.io/crates/erc8128
[doc-badge]: https://img.shields.io/docsrs/erc8128.svg
[doc-url]: https://docs.rs/erc8128
[license-badge]: https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg
[license-url]: LICENSE-MIT
[rust-badge]: https://img.shields.io/badge/rust-edition%202024-orange.svg
[rust-url]: https://doc.rust-lang.org/edition-guide/
Type-safe Rust SDK for [ERC-8128][spec]: HTTP request authentication via
[RFC 9421][rfc9421] message signatures with Ethereum accounts (EOA & ERC-1271).
[Quick Start](#quick-start) | [Features](#feature-flags) | [Protocol](#erc-8128-protocol) | [Examples](#examples) | [API Reference][doc-url]
## Quick Start
```toml
[dependencies]
erc8128 = { version = "0.3", features = ["k256"] }
```
### Sign & Verify (in-memory roundtrip)
```rust
use erc8128::{
MemoryNonceStore, RejectReplayable, Request, SignOptions, VerifyPolicy,
eoa::{EoaSigner, EoaVerifier},
sign_request, verify_request,
};
let signer = EoaSigner::from_slice(&private_key_bytes, 1)?;
let request = Request {
method: "POST",
url: "https://api.example.com/orders",
headers: &[("content-type", "application/json")],
body: Some(b"{\"item\":\"widget\"}"),
};
// Sign
let signed = sign_request(&request, &signer, &SignOptions::default()).await?;
// Attach signature headers, then verify
let nonces = MemoryNonceStore::default();
let result = verify_request(
&verify_req, &EoaVerifier, &nonces, &RejectReplayable, &VerifyPolicy::default(),
).await?;
println!("Authenticated: {} on chain {}", result.address, result.chain_id);
```
### reqwest Client
```toml
erc8128 = { version = "0.3", features = ["k256", "reqwest"] }
```
```rust
use erc8128::{SignOptions, client::signed_fetch, eoa::EoaSigner};
let signer = EoaSigner::from_slice(&key, 1)?;
let resp = signed_fetch(
&reqwest::Client::new(),
reqwest::Method::POST,
"https://api.example.com/orders",
&[("content-type", "application/json")],
Some(b"{\"item\":\"widget\"}"),
&signer,
&SignOptions::default(),
).await?;
```
### axum Server
```toml
erc8128 = { version = "0.3", features = ["k256", "axum"] }
```
```rust
use axum::{Router, routing::post, Extension};
use erc8128::{
MemoryNonceStore, RejectReplayable, VerifyPolicy, VerifySuccess,
eoa::EoaVerifier, middleware::Erc8128Layer,
};
let app = Router::new()
.route("/api", post(handler))
.layer(Erc8128Layer::new(
EoaVerifier,
MemoryNonceStore::default(),
RejectReplayable,
VerifyPolicy::default(),
));
async fn handler(Extension(auth): Extension) -> String {
format!("Hello, {}!", auth.address)
}
```
## Feature Flags
| Feature | Dependencies | Provides |
| --- | --- | --- |
| `k256` | `k256` | `eoa::EoaSigner` + `eoa::EoaVerifier` — pure-Rust EOA signing & verification |
| `alloy` | `alloy-signer` | `alloy::AlloySigner` — adapter for any `alloy_signer::Signer` |
| `axum` | `axum`, `tower` | `middleware::Erc8128Layer` — Tower middleware for automatic request verification |
| `reqwest` | `reqwest` | `client::signed_fetch` + `client::RequestBuilderExt` — signed HTTP requests |
The core crate (`sign_request`, `verify_request`, traits) has **zero** HTTP framework dependencies.
## Examples
```sh
cargo run --example roundtrip --features k256 # in-memory sign → verify
cargo run --example reqwest_client --features k256,reqwest # client-side signing
cargo run --example axum_server --features k256,axum # server-side middleware
cargo run --example e2e --features k256,axum,reqwest # full end-to-end demo
```
The `e2e` example starts an axum server and a reqwest client in one process, demonstrating:
1. Fresh signature → 200 OK
2. Replay of the same signature → 401 (nonce already consumed)
3. New signature with fresh nonce → 200 OK
## ERC-8128 Protocol
### Signature Flow
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>C: Construct RFC 9421 signature base
C->>C: EIP-191 personal_sign(signature_base)
C->>S: HTTP Request + Signature-Input + Signature + Content-Digest
S->>S: Parse Signature-Input
S->>S: Reconstruct signature base from request
S->>S: Verify Content-Digest (SHA-256 / SHA-512)
S->>S: ecrecover / ERC-1271 verify
S->>S: Nonce & expiry checks
S-->>C: Authenticated response
```
### Binding Modes
| Mode | Covered Components | Semantics |
| --- | --- | --- |
| **Request-Bound** | `@method` `@authority` `@path` `@query` `content-digest` | Authorizes **exactly one** concrete HTTP request. |
| **Class-Bound** | Caller-defined set (must include `@authority`) | Authorizes **a class** of requests matching the covered components. |
### Replay Protection
| Mode | Nonce | Semantics |
| --- | --- | --- |
| **Non-Replayable** | Present | Each signature consumed **exactly once** via `NonceStore`. |
| **Replayable** | Absent | Valid within the `[created, expires]` window. Requires [`ReplayablePolicy`][doc-url] with early invalidation hooks (Section 5.2). |
### Signature Parameters (RFC 9421)
| Parameter | Required | Description |
| --- | --- | --- |
| `created` | Yes | Unix timestamp of signature creation. |
| `expires` | Yes | Unix timestamp of signature expiration (`> created`). |
| `keyid` | Yes | Signer identity: `erc8128::`. |
| `nonce` | Non-Replayable | Cryptographically random replay-prevention token. |
| `tag` | No | Application-level discriminator for routing. |
### Content-Digest
Body integrity is protected via `Content-Digest` (SHA-256 or SHA-512). Four modes control signing behavior:
| Mode | Behavior |
| --- | --- |
| **`Auto`** | Use an existing header, or compute one from the body. *(default)* |
| **`Recompute`** | Always recompute, overwriting any existing header. |
| **`Require`** | Fail if the header is absent — never compute. |
| **`Off`** | Disable entirely — fail if body-bound components are present. |
## Extensibility
The SDK delegates all environment-specific logic to pluggable traits:
| Trait | Responsibility | Built-in |
| --- | --- | --- |
| [`Signer`][doc-url] | Produce EIP-191 `personal_sign` signatures. | `EoaSigner` (k256), `AlloySigner` (alloy) |
| [`Verifier`][doc-url] | Verify signatures: `(address, message, signature)`. | `EoaVerifier` (ecrecover) |
| [`NonceStore`][doc-url] | Atomically consume nonces for replay protection. | `MemoryNonceStore`, `NoNonceStore` |
| [`ReplayablePolicy`][doc-url] | Early invalidation hooks for replayable signatures (Section 5.2). | `RejectReplayable` |
## Related Standards
| Standard | Relationship |
| --- | --- |
| [RFC 9421][rfc9421] | HTTP Message Signatures — the wire format ERC-8128 builds on |
| [EIP-191][eip191] | Signed Data Standard — `personal_sign` message prefix |
| [ERC-1271][erc1271] | Standard Signature Validation for smart contract accounts |
| [ERC-6492][erc6492] | Signature Validation for pre-deployed (counterfactual) contracts |
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or )
- MIT License ([LICENSE-MIT](LICENSE-MIT) or )
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.
---
A **[QNTX](https://qntx.fun)** open-source project.

Code is law. We write both.
[spec]: https://erc8128.org
[rfc9421]: https://www.rfc-editor.org/rfc/rfc9421
[eip191]: https://eips.ethereum.org/EIPS/eip-191
[erc1271]: https://eips.ethereum.org/EIPS/eip-1271
[erc6492]: https://eips.ethereum.org/EIPS/eip-6492