A batteries included HTTP/1.1 client in OCaml
at main 480 lines 16 kB view raw
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. *)