Skip to main content

wimsey_httpsig/
lib.rs

1//! `wimsey-httpsig` — the WIMSE HTTP Message Signatures transport binding.
2//!
3//! Target spec: `draft-ietf-wimse-http-signature-06`, a profile of RFC 9421.
4//! The calling workload signs the outgoing HTTP request — including the header
5//! that carries its WIT — with its proof-of-possession key, so an intermediary
6//! can read but not tamper with the covered components. The receiver recovers
7//! the key from the WIT's `cnf` claim and verifies the signature.
8//!
9//! This crate implements the RFC 9421 signature base (Section 2.5) for the
10//! derived components `@method`, `@authority`, `@path`, `@query` and
11//! `@request-target` plus header fields, signs with Ed25519, and serializes the
12//! `Signature-Input` and `Signature` fields. The signature base is verified
13//! byte-for-byte against the RFC's worked example.
14//!
15//! # The WIMSE profile
16//!
17//! Section 3 of the draft narrows RFC 9421 considerably. Set
18//! [`VerifyConfig::wimse_profile`] to enforce it, or call
19//! [`check_request_profile`] directly:
20//!
21//! - `@method` and `@request-target` MUST be covered, along with `Content-Type`,
22//!   `Content-Digest`, `Authorization`, `Txn-Token` and `Workload-Identity-Token`
23//!   whenever the message carries them.
24//! - `created`, `expires`, `nonce` and `tag` MUST all be present, with `tag`
25//!   equal to [`WIMSE_TAG`] and a tight `expires` window (minutes, not hours).
26//! - `wimse-aud` MUST be present on a request, naming the service the signature
27//!   is for. A verifier binds itself to that audience with
28//!   [`VerifyConfig::expected_audience`].
29//! - `keyid` and `alg` MUST NOT be used: the key travels in the WIT and its
30//!   `cnf` JWK pins the algorithm, so repeating either would only add confusion.
31//!
32//! The profile is off by default, so the crate can also be driven as a plain
33//! RFC 9421 implementation.
34//!
35//! # Caller responsibilities and limitations
36//!
37//! - Verifying a signature proves only that the covered components were signed.
38//!   Set [`VerifyConfig::required_components`] to demand the components you care
39//!   about.
40//! - Covering `content-digest` protects only the header string. To bind the
41//!   body, also call [`verify_content_digest`] over the received body.
42//! - Exactly one signature per `Signature`/`Signature-Input` field is supported.
43//! - `@authority` is lowercased but its default port is not stripped; pass a
44//!   normalized authority.
45//! - Response signing (`@status`, `;req` components and `wimse-req-nonce`) is
46//!   not implemented yet; [`SignatureParams::wimse_req_nonce`] is carried and
47//!   verified, but this crate models requests only.
48//! - Replay defense is the caller's: this crate checks that a `nonce` is present
49//!   but does not remember the ones it has seen.
50//!
51//! ```
52//! use ed25519_dalek::SigningKey;
53//! use wimsey_httpsig::{
54//!     content_digest_sha256, sign, verify, verify_content_digest, Component, HttpRequest,
55//!     SignatureParams, VerifyConfig, WIMSE_TAG,
56//! };
57//!
58//! let pop_key = SigningKey::from_bytes(&[5u8; 32]);
59//! let body = br#"{"hello":"world"}"#;
60//!
61//! let request = HttpRequest {
62//!     method: "POST".to_owned(),
63//!     authority: "service.example".to_owned(),
64//!     path: "/transfer".to_owned(),
65//!     query: None,
66//!     headers: vec![
67//!         ("Content-Digest".to_owned(), content_digest_sha256(body)),
68//!         ("Workload-Identity-Token".to_owned(), "eyJ0eXAi.wit.value".to_owned()),
69//!     ],
70//! };
71//! let components = vec![
72//!     Component::Method,
73//!     Component::RequestTarget,
74//!     Component::header("content-digest"),
75//!     Component::header("workload-identity-token"),
76//! ];
77//! let params = SignatureParams {
78//!     created: Some(1_700_000_000),
79//!     expires: Some(1_700_000_300),
80//!     nonce: Some("abcd1111".to_owned()),
81//!     tag: Some(WIMSE_TAG.to_owned()),
82//!     wimse_aud: Some("https://service.example/transfer".to_owned()),
83//!     ..SignatureParams::default()
84//! };
85//!
86//! let signed = sign(&request, &components, &params, "wimse", &pop_key).unwrap();
87//!
88//! // The receiver enforces the profile, pins the audience it answers to, and
89//! // binds the body by checking the content-digest against it.
90//! let config = VerifyConfig {
91//!     now: Some(1_700_000_030),
92//!     required_components: components.clone(),
93//!     wimse_profile: true,
94//!     expected_audience: Some("https://service.example/transfer".to_owned()),
95//!     ..VerifyConfig::default()
96//! };
97//! let verified =
98//!     verify(&request, &signed.signature_input, &signed.signature, &pop_key.verifying_key(), &config)
99//!         .unwrap();
100//! assert_eq!(verified.label, "wimse");
101//! assert!(verify_content_digest("sha-256=:invalid:", body) == false);
102//! ```
103
104mod error;
105mod message;
106mod signature;
107
108pub use error::HttpSigError;
109pub use message::{content_digest_sha256, verify_content_digest, Component, HttpRequest};
110pub use signature::{
111    check_request_profile, sign, signature_base, verify, SignatureParams, SignedSignature,
112    VerifiedSignature, VerifyConfig, ALG, WIMSE_LABEL, WIMSE_TAG,
113};
114
115// Re-exported so callers can name the key types without a direct dependency.
116pub use ed25519_dalek::{SigningKey, VerifyingKey};