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 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, &params, "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};