A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** RFC 9421 HTTP Message Signatures
7
8 This module implements {{:https://datatracker.ietf.org/doc/html/rfc9421}RFC 9421}
9 HTTP Message Signatures, enabling cryptographic signing and verification of
10 HTTP request and response messages.
11
12 {2 Overview}
13
14 HTTP Message Signatures provide a mechanism to create and verify cryptographic
15 signatures on HTTP messages. The signature covers specific message components
16 (headers, derived values) and metadata (timestamps, key identifiers).
17
18 {2 Example Usage}
19
20 {[
21 Eio_main.run @@ fun env ->
22 let clock = Eio.Stdenv.clock env in
23
24 (* Create a signing configuration *)
25 let key = Signature.Key.ed25519
26 ~priv:(Base64.decode_exn "private-key-bytes")
27 ~pub:(Base64.decode_exn "public-key-bytes") in
28 let config = Signature.config
29 ~key
30 ~keyid:"my-key-id"
31 ~components:[Signature.Component.method_; Signature.Component.authority]
32 () in
33
34 (* Sign headers *)
35 let ctx = Signature.Context.request ~method_:`GET ~uri:(Uri.of_string "https://example.com/") ~headers in
36 let signed_headers = Signature.sign ~clock ~config ~context:ctx ~headers |> Result.get_ok in
37
38 (* Verify signatures *)
39 let result = Signature.verify ~clock ~key ~context:ctx ~headers:signed_headers () in
40 ...
41 ]}
42
43 {2 References}
44 {ul
45 {- {{:https://datatracker.ietf.org/doc/html/rfc9421}RFC 9421} - HTTP Message Signatures}
46 {- {{:https://datatracker.ietf.org/doc/html/rfc9530}RFC 9530} - Digest Fields}} *)
47
48(** {1 Algorithms} *)
49
50module Algorithm : sig
51 (** Signature algorithms per
52 {{:https://datatracker.ietf.org/doc/html/rfc9421#section-3.3}RFC 9421 Section 3.3}. *)
53
54 type t = [
55 | `Rsa_pss_sha512 (** RSASSA-PSS using SHA-512 *)
56 | `Rsa_v1_5_sha256 (** RSASSA-PKCS1-v1_5 using SHA-256 *)
57 | `Hmac_sha256 (** HMAC using SHA-256 *)
58 | `Ecdsa_p256_sha256 (** ECDSA using P-256 curve with SHA-256 *)
59 | `Ecdsa_p384_sha384 (** ECDSA using P-384 curve with SHA-384 *)
60 | `Ed25519 (** EdDSA using curve25519 *)
61 ]
62
63 val to_string : t -> string
64 (** [to_string alg] returns the algorithm identifier string. *)
65
66 val of_string : string -> t option
67 (** [of_string s] parses an algorithm identifier string. *)
68
69 val of_string_exn : string -> t
70 (** [of_string_exn s] parses an algorithm identifier or raises [Invalid_argument]. *)
71end
72
73(** {1 Message Components} *)
74
75module Component : sig
76 (** Message components that can be included in signatures per
77 {{:https://datatracker.ietf.org/doc/html/rfc9421#section-2}RFC 9421 Section 2}. *)
78
79 (** {2 Derived Components}
80
81 Derived components are computed from message context, not raw headers.
82 Per {{:https://datatracker.ietf.org/doc/html/rfc9421#section-2.2}Section 2.2}. *)
83
84 type derived = [
85 | `Method (** [@method] - HTTP request method *)
86 | `Authority (** [@authority] - Target host (host:port) *)
87 | `Path (** [@path] - Request target path *)
88 | `Query (** [@query] - Query string with leading [?] *)
89 | `Query_param of string (** [@query-param;name="..."] - Individual query parameter *)
90 | `Target_uri (** [@target-uri] - Full target URI *)
91 | `Status (** [@status] - Response status code (responses only) *)
92 | `Request_target (** [@request-target] - Deprecated form *)
93 ]
94
95 (** {2 Component Parameters}
96
97 Parameters that modify component behavior per
98 {{:https://datatracker.ietf.org/doc/html/rfc9421#section-2.1}Section 2.1}. *)
99
100 type param = [
101 | `Sf (** Strict structured field serialization *)
102 | `Key of string (** Dictionary member selection *)
103 | `Bs (** Byte sequence wrapping *)
104 | `Tr (** Trailer field designation *)
105 | `Req (** Request-bound component (for response signatures) *)
106 ]
107
108 (** A component identifier, either derived or a header field. *)
109 type t = [
110 | `Derived of derived * param list
111 | `Field of string * param list
112 ]
113
114 (** {2 Constructors} *)
115
116 val method_ : t
117 (** The [@method] derived component. *)
118
119 val authority : t
120 (** The [@authority] derived component. *)
121
122 val path : t
123 (** The [@path] derived component. *)
124
125 val query : t
126 (** The [@query] derived component. *)
127
128 val query_param : string -> t
129 (** [query_param name] creates a [@query-param;name="..."] component. *)
130
131 val target_uri : t
132 (** The [@target-uri] derived component. *)
133
134 val status : t
135 (** The [@status] derived component (for responses). *)
136
137 val request_target : t
138 (** The [@request-target] derived component (deprecated). *)
139
140 val field : string -> t
141 (** [field name] creates a header field component (lowercased). *)
142
143 val field_sf : string -> t
144 (** [field_sf name] creates a header field with strict structured field serialization. *)
145
146 val field_bs : string -> t
147 (** [field_bs name] creates a header field with byte sequence wrapping. *)
148
149 val field_key : string -> key:string -> t
150 (** [field_key name ~key] creates a header field selecting a dictionary member. *)
151
152 val field_req : string -> t
153 (** [field_req name] creates a request-bound header field (for responses). *)
154
155 (** {2 Common Fields} *)
156
157 val content_type : t
158 (** The [content-type] header field. *)
159
160 val content_length : t
161 (** The [content-length] header field. *)
162
163 val content_digest : t
164 (** The [content-digest] header field (RFC 9530). *)
165
166 val date : t
167 (** The [date] header field. *)
168
169 val host : t
170 (** The [host] header field. *)
171
172 (** {2 Serialization} *)
173
174 val to_identifier : t -> string
175 (** [to_identifier c] returns the component identifier string. *)
176
177 val of_identifier : string -> (t, string) result
178 (** [of_identifier s] parses a component identifier. *)
179end
180
181(** {1 Signature Parameters} *)
182
183module Params : sig
184 (** Signature parameters per
185 {{:https://datatracker.ietf.org/doc/html/rfc9421#section-2.3}RFC 9421 Section 2.3}. *)
186
187 type t
188 (** Signature parameters. *)
189
190 val empty : t
191 (** Empty parameters. *)
192
193 val created : Ptime.t -> t -> t
194 (** [created time params] sets the creation timestamp. *)
195
196 val expires : Ptime.t -> t -> t
197 (** [expires time params] sets the expiration timestamp. *)
198
199 val nonce : string -> t -> t
200 (** [nonce value params] sets a unique nonce. *)
201
202 val alg : Algorithm.t -> t -> t
203 (** [alg algorithm params] sets the algorithm identifier. *)
204
205 val keyid : string -> t -> t
206 (** [keyid id params] sets the key identifier. *)
207
208 val tag : string -> t -> t
209 (** [tag value params] sets an application-specific tag. *)
210
211 val get_created : t -> Ptime.t option
212 val get_expires : t -> Ptime.t option
213 val get_nonce : t -> string option
214 val get_alg : t -> Algorithm.t option
215 val get_keyid : t -> string option
216 val get_tag : t -> string option
217end
218
219(** {1 Key Material} *)
220
221module Key : sig
222 (** Cryptographic key material for signing and verification. *)
223
224 type t
225 (** A key (may contain private key, public key, or both). *)
226
227 (** {2 Symmetric Keys} *)
228
229 val symmetric : string -> t
230 (** [symmetric secret] creates a symmetric key for HMAC algorithms. *)
231
232 (** {2 Ed25519 Keys} *)
233
234 val ed25519 : priv:string -> pub:string -> t
235 (** [ed25519 ~priv ~pub] creates an Ed25519 key pair.
236 Both [priv] and [pub] should be raw 32-byte keys. *)
237
238 val ed25519_priv : string -> t
239 (** [ed25519_priv priv] creates an Ed25519 private key (for signing only). *)
240
241 val ed25519_pub : string -> t
242 (** [ed25519_pub pub] creates an Ed25519 public key (for verification only). *)
243
244 (** {2 ECDSA P-256 Keys} *)
245
246 val p256 : priv:Mirage_crypto_ec.P256.Dsa.priv -> t
247 (** [p256 ~priv] creates a P-256 key from the private key
248 (public key derived automatically). *)
249
250 val p256_pub : Mirage_crypto_ec.P256.Dsa.pub -> t
251 (** [p256_pub pub] creates a P-256 public key (for verification only). *)
252
253 (** {2 ECDSA P-384 Keys} *)
254
255 val p384 : priv:Mirage_crypto_ec.P384.Dsa.priv -> t
256 (** [p384 ~priv] creates a P-384 key from the private key. *)
257
258 val p384_pub : Mirage_crypto_ec.P384.Dsa.pub -> t
259 (** [p384_pub pub] creates a P-384 public key (for verification only). *)
260
261 (** {2 RSA Keys} *)
262
263 val rsa : priv:Mirage_crypto_pk.Rsa.priv -> t
264 (** [rsa ~priv] creates an RSA key from the private key. *)
265
266 val rsa_pub : Mirage_crypto_pk.Rsa.pub -> t
267 (** [rsa_pub pub] creates an RSA public key (for verification only). *)
268
269 (** {2 Key Properties} *)
270
271 val can_sign : t -> bool
272 (** [can_sign key] returns [true] if the key can be used for signing. *)
273
274 val can_verify : t -> bool
275 (** [can_verify key] returns [true] if the key can be used for verification. *)
276
277 val algorithm : t -> Algorithm.t option
278 (** [algorithm key] returns the algorithm associated with the key, if known. *)
279end
280
281(** {1 Signing Context} *)
282
283type request_ctx = {
284 method_ : Method.t;
285 uri : Uri.t;
286 headers : Headers.t;
287}
288(** Request context for signature computation.
289 Contains the HTTP method, request URI, and request headers. *)
290
291type response_ctx = {
292 status : int;
293 headers : Headers.t;
294 request : request_ctx option;
295}
296(** Response context for signature computation.
297 Contains the HTTP status code, response headers, and optionally the original request. *)
298
299(** Context for resolving message components. *)
300module Context : sig
301 type t = [
302 | `Request of request_ctx
303 | `Response of response_ctx
304 ]
305 (** Message context (request or response). *)
306
307 val request :
308 method_:Method.t ->
309 uri:Uri.t ->
310 headers:Headers.t ->
311 t
312 (** [request ~method_ ~uri ~headers] creates a request context. *)
313
314 val response :
315 status:int ->
316 headers:Headers.t ->
317 ?request:t ->
318 unit ->
319 t
320 (** [response ~status ~headers ?request ()] creates a response context.
321 The optional [request] context is used for request-bound components. *)
322end
323
324(** {1 Content-Digest} *)
325
326module Content_digest : sig
327 (** RFC 9530 Content-Digest support.
328
329 {{:https://datatracker.ietf.org/doc/html/rfc9530}RFC 9530} defines the
330 Content-Digest header for message body integrity. *)
331
332 type algorithm = [ `Sha256 | `Sha512 ]
333
334 val compute : algorithm:algorithm -> body:string -> string
335 (** [compute ~algorithm ~body] returns the Content-Digest header value. *)
336
337 val add : algorithm:algorithm -> body:string -> Headers.t -> Headers.t
338 (** [add ~algorithm ~body headers] adds a Content-Digest header. *)
339
340 val verify : header:string -> body:string -> (unit, string) result
341 (** [verify ~header ~body] verifies a Content-Digest header value. *)
342end
343
344(** {1 Signing Configuration} *)
345
346type config
347(** Configuration for signing requests. *)
348
349val config :
350 key:Key.t ->
351 ?keyid:string ->
352 ?components:Component.t list ->
353 ?tag:string ->
354 ?include_created:bool ->
355 ?label:string ->
356 unit ->
357 config
358(** [config ~key ?keyid ?components ?tag ?include_created ?label ()]
359 creates a signing configuration.
360
361 @param key The signing key.
362 @param keyid Key identifier (included in signature parameters).
363 @param components Components to sign. Default: [[\@method; \@authority; \@path]].
364 @param tag Application-specific tag.
365 @param include_created Include creation timestamp. Default: [true].
366 @param label Signature label for dictionary key. Default: ["sig1"]. *)
367
368val default_components : Component.t list
369(** Default components to sign: [[\@method; \@authority; \@path]]. *)
370
371(** {1 Signing} *)
372
373type sign_error = [
374 | `Key_algorithm_mismatch of string
375 | `Missing_private_key
376 | `Component_resolution_error of string
377 | `Crypto_error of string
378]
379
380val sign_error_to_string : sign_error -> string
381(** [sign_error_to_string err] returns a human-readable error message. *)
382
383val sign :
384 clock:_ Eio.Time.clock ->
385 config:config ->
386 context:Context.t ->
387 headers:Headers.t ->
388 (Headers.t, sign_error) result
389(** [sign ~clock ~config ~context ~headers] signs the message and returns
390 headers with [Signature-Input] and [Signature] headers added.
391
392 @param clock Eio clock used for the [created] timestamp. *)
393
394val sign_with_digest :
395 clock:_ Eio.Time.clock ->
396 config:config ->
397 context:Context.t ->
398 headers:Headers.t ->
399 body:string ->
400 digest_algorithm:Content_digest.algorithm ->
401 (Headers.t, sign_error) result
402(** [sign_with_digest ~clock ~config ~context ~headers ~body ~digest_algorithm]
403 computes Content-Digest, adds it to headers, and signs.
404 The [content-digest] component is automatically added to signed components.
405
406 @param clock Eio clock used for the [created] timestamp. *)
407
408(** {1 Verification} *)
409
410type verify_error = [
411 | `Missing_signature_header
412 | `Missing_signature_input_header
413 | `Invalid_signature_input of string
414 | `Signature_label_not_found of string
415 | `Key_algorithm_mismatch of string
416 | `Missing_public_key
417 | `Component_resolution_error of string
418 | `Signature_mismatch
419 | `Signature_expired
420 | `Required_component_missing of string
421 | `Crypto_error of string
422]
423
424val verify_error_to_string : verify_error -> string
425(** [verify_error_to_string err] returns a human-readable error message. *)
426
427type verify_result = {
428 label : string;
429 keyid : string option;
430 created : Ptime.t option;
431 expires : Ptime.t option;
432 verified_components : Component.t list;
433}
434(** Successful verification result. *)
435
436val verify :
437 clock:_ Eio.Time.clock ->
438 key:Key.t ->
439 ?label:string ->
440 ?max_age:Ptime.Span.t ->
441 ?required_components:Component.t list ->
442 context:Context.t ->
443 headers:Headers.t ->
444 unit ->
445 (verify_result, verify_error) result
446(** [verify ~clock ~key ?label ?max_age ?required_components ~context ~headers]
447 verifies a signature on the message.
448
449 Time validation per RFC 9421:
450 - If [expires] is present and in the past, verification fails
451 - If [created] is present and in the future (with 60s clock skew allowance),
452 verification fails
453 - If [max_age] is specified and [created] is older than [max_age],
454 verification fails
455
456 @param clock Eio clock used for time validation.
457 @param key The verification key.
458 @param label Signature label to verify. Default: first signature found.
459 @param max_age Maximum age of signature. If [created] is present and older
460 than [max_age], verification fails.
461 @param required_components Components that must be signed. Verification
462 fails if any are missing from the signature.
463 @param context Message context for component resolution.
464 @param headers Headers containing [Signature] and [Signature-Input]. *)
465
466val verify_all :
467 clock:_ Eio.Time.clock ->
468 key_resolver:(string -> Key.t option) ->
469 ?max_age:Ptime.Span.t ->
470 context:Context.t ->
471 headers:Headers.t ->
472 unit ->
473 (verify_result list, verify_error) result
474(** [verify_all ~clock ~key_resolver ?max_age ~context ~headers] verifies all
475 signatures on a message.
476
477 @param clock Eio clock used for time validation.
478 @param key_resolver Function to resolve keys by [keyid]. Called for
479 each signature with its [keyid] parameter.
480 @param max_age Maximum age for all signatures. *)