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 is supported: sign an [`HttpExchange`] rather than an
46//! [`HttpRequest`], enforce [`VerifyConfig::wimse_response_profile`], and
47//! check the returned nonce with [`VerifyConfig::expected_req_nonce`].
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 wimsey_httpsig::{
53//! content_digest_sha256, sign, verify, verify_content_digest, Component, HttpRequest,
54//! SignatureParams, SigningKey, VerifyConfig, WIMSE_TAG,
55//! };
56//!
57//! let pop_key = SigningKey::from_ed25519_seed(&[5u8; 32]);
58//! let body = br#"{"hello":"world"}"#;
59//!
60//! let request = HttpRequest {
61//! method: "POST".to_owned(),
62//! authority: "service.example".to_owned(),
63//! path: "/transfer".to_owned(),
64//! query: None,
65//! headers: vec![
66//! ("Content-Digest".to_owned(), content_digest_sha256(body)),
67//! ("Workload-Identity-Token".to_owned(), "eyJ0eXAi.wit.value".to_owned()),
68//! ],
69//! };
70//! let components = vec![
71//! Component::Method,
72//! Component::RequestTarget,
73//! Component::header("content-digest"),
74//! Component::header("workload-identity-token"),
75//! ];
76//! let params = SignatureParams {
77//! created: Some(1_700_000_000),
78//! expires: Some(1_700_000_300),
79//! nonce: Some("abcd1111".to_owned()),
80//! tag: Some(WIMSE_TAG.to_owned()),
81//! wimse_aud: Some("https://service.example/transfer".to_owned()),
82//! ..SignatureParams::default()
83//! };
84//!
85//! let signed = sign(&request, &components, ¶ms, "wimse", &pop_key).unwrap();
86//!
87//! // The receiver enforces the profile, pins the audience it answers to, and
88//! // binds the body by checking the content-digest against it.
89//! let config = VerifyConfig {
90//! now: Some(1_700_000_030),
91//! required_components: components.clone(),
92//! wimse_profile: true,
93//! expected_audience: Some("https://service.example/transfer".to_owned()),
94//! ..VerifyConfig::default()
95//! };
96//! let verified =
97//! verify(&request, &signed.signature_input, &signed.signature, &pop_key.verifying_key(), &config)
98//! .unwrap();
99//! assert_eq!(verified.label, "wimse");
100//! assert!(verify_content_digest("sha-256=:invalid:", body) == false);
101//! ```
102
103mod error;
104mod message;
105mod signature;
106
107pub use error::HttpSigError;
108pub use message::{
109 content_digest_sha256, verify_content_digest, Component, ComponentSource, HttpExchange,
110 HttpRequest, HttpResponse,
111};
112pub use signature::{
113 check_request_profile, check_response_profile, response_components, sign, signature_base,
114 verify, SignatureParams, SignedSignature, VerifiedSignature, VerifyConfig, ALG, WIMSE_LABEL,
115 WIMSE_TAG,
116};
117
118// Re-exported so callers can name the key types without a direct dependency.
119pub use wimsey_jose::{Algorithm, SigningKey, VerifyingKey};