http-message-signatures: An implementation of RFC 9421, the IETF HTTP Message Signatures standard
http-message-signatures is an implementation of the IETF RFC 9421 HTTP Message Signatures standard in Python.
Installation
pip install http-message-signatures
Synopsis
import hashlib
import requests
from http_sf.compat import Dictionary
from http_message_signatures import HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureKeyResolver, algorithms
class MyHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
keys = {"my-key": b"top-secret-key"}
def resolve_public_key(self, key_id: str):
return self.keys[key_id]
def resolve_private_key(self, key_id: str):
return self.keys[key_id]
request = requests.Request('POST', 'https://example.com/foo?param=Value&Pet=dog', json={"hello": "world"})
request = request.prepare()
request.headers["Content-Digest"] = str(Dictionary({"sha-256": hashlib.sha256(request.body).digest()}))
signer = HTTPMessageSigner(signature_algorithm=algorithms.HMAC_SHA256, key_resolver=MyHTTPSignatureKeyResolver())
signer.sign(request, key_id="my-key", covered_component_ids=("@method", "@authority", "@target-uri", "content-digest"))
verifier = HTTPMessageVerifier(signature_algorithm=algorithms.HMAC_SHA256, key_resolver=MyHTTPSignatureKeyResolver())
verifier.verify(request)
Note that verifying the body content-digest is outside the scope of this package’s functionality, so it remains the caller’s responsibility. The requests-http-signature library builds upon this package to provide integrated signing and validation of the request body.
Remote and asynchronous signing
Use prepare() and apply() when a KMS, HSM, SSH agent, or asynchronous client holds the signing key. prepare() builds the RFC 9421 signature base without reading a private key or modifying the message. It accepts the signing options used by sign(), including covered components, creation and expiration times, nonce, tag, label, and include_alg. The returned PreparedSignature contains immutable bytes and strings. apply() writes the Signature-Input and Signature headers after the external signer returns.
This example expects an asynchronous sign_base function that signs the supplied bytes with Ed25519 and returns the raw 64-byte signature. The client must use the private key identified by key_id:
from collections.abc import Awaitable, Callable
from http_message_signatures import HTTPMessageSigner, algorithms
async def sign_request(request, key_id: str, sign_base: Callable[[bytes], Awaitable[bytes]]):
signer = HTTPMessageSigner(signature_algorithm=algorithms.ED25519)
prepared = signer.prepare(
request,
key_id=key_id,
label="sig1",
covered_component_ids=("@method", "@authority", "@target-uri", "content-digest"),
)
signature = await sign_base(prepared.signature_base)
signer.apply(request, prepared, signature)
Populate Content-Digest from the request body before this example runs, as in the synopsis above. The same two calls work with a synchronous signer or a signing job completed in another process. No local key_resolver is needed. The existing sign() method still resolves a local key and performs all three steps synchronously.
Pass prepared.signature_base to the signing algorithm as bytes, without base64 encoding or a trailing newline. prepared.signature_params is the serialized @signature-params value; it is useful for inspecting the covered component list and signed metadata. Do not rebuild either value yourself. The selected algorithm applies to the remote signer even when include_alg=False; omitting the parameter does not negotiate an algorithm.
Remote clients differ in their accepted input and output formats. Configure the algorithm as specified in RFC 9421 section 3.3. Ed25519 signs the base without prehashing. For RSA and ECDSA, a client that accepts raw messages must perform the specified hashing; a client that accepts a digest must receive that digest and be configured to avoid hashing it a second time. The RSA-PSS profile uses SHA-512, MGF1 with SHA-512, and a 64-byte salt.
apply() expects raw signature bytes and performs the Structured Field base64 encoding. Decode base64 responses from a service before calling it. ECDSA P-256 clients often return DER, while RFC 9421 requires a 64-byte concatenation of r and s, each padded to 32 bytes. Convert DER output using the algorithm’s helper:
signer = HTTPMessageSigner(signature_algorithm=algorithms.ECDSA_P256_SHA256) prepared = signer.prepare(request, key_id="p256-key") # der_signature is the client's ECDSA P-256/SHA-256 signature of prepared.signature_base. signature = algorithms.ECDSA_P256_SHA256.signature_from_der(der_signature) signer.apply(request, prepared, signature)
Retain the prepared object and associate each remote result with its input. Use a trusted signing service and protect queued signing input against modification and disclosure: the base contains covered header and URL values. apply() compares the message’s current signature base with the prepared bytes and rejects changes before writing headers. For example, changing a covered method from POST to DELETE while awaiting the signature is rejected. Changes to uncovered values or values with the same RFC normalization are permitted. This check does not authenticate the prepared object, verify the returned signature, or check a body against its digest. Applications must handle those responsibilities and keep the message stable during apply() and until it is sent.
Creation and expiration times are set during preparation and are not refreshed by apply(). Allow for signing delays when choosing expiration times; an expired result will still be rejected by a verifier. To add the result alongside existing signatures, pass append_if_signature_exists=True to apply(). Duplicate labels or malformed existing signature headers are rejected. By default it replaces existing signature headers, just like sign().
URL components
The request’s url supplies the target URL. URL components preserve percent-encoded octets and distinguish an absent query from an empty query. @authority omits credentials and default ports; @target-uri excludes credentials and fragments. Empty paths normally become /. For OPTIONS, @target-uri preserves an empty path so server-wide OPTIONS * remains distinct from OPTIONS /. The separate @path component still represents an empty path as /.
Supply an encoded absolute URL with a nonempty host. Parsing preserves the supplied spelling and rejects invalid syntax, including malformed percent escapes (%, %2, or %ZZ), spaces, control characters, and unencoded non-ASCII characters. It never repairs input. Each signature component applies its own RFC 9421 formatting rules. This applies during signing, verification, applying a prepared signature, and resolving request components covered by a response signature. A response’s own url is not used; components marked req use its associated request.
For @request-target, clients can supply message.request_target containing the HTTP/1.1 request target. This is necessary for absolute-form proxy requests and asterisk-form OPTIONS * requests, since those forms cannot be inferred from an absolute URL. Without this attribute, the resolver derives origin form from the URL, or authority form for CONNECT. The supplied target and URL must describe the same request.
Multiple signatures
The message’s headers.items() must expose every field line, or values already combined in wire order. Repeated field names are compared case-insensitively, normalized individually, and joined with a comma followed by a space before signing or parsing Structured Fields. Containers that discard repeated field lines before passing them to this library cannot be used to verify those fields reliably. Signing also requires header assignment and deletion; existing Signature and Signature-Input field lines are replaced together, including when appending a signature.
An HTTP request can potentially have multiple signatures. By default, http-message-signatures overwrites any existing signature when signing, and requires exactly one signature to be present when verifying. Before enabling multi-signature support, please read section 7.2.6 of the RFC, Multiple Signature Confusion.
To append a signature to a message that might already carry an existing signature without overwriting it, use:
signer.sign(request, append_if_signature_exists=True, ...)
To identify a signature using a tag parameter and verify it on a message possibly carrying multiple signatures, use:
verifier.verify(request, expect_tag="my_app_tag")
This will filter all signatures down to only those that set the tag to the expected value, verify each of them, and return a list of VerifyResult s. Verifying multiple signatures without prior knowledge of the application tag is not supported.
Error handling
Input request methods must be uppercase strings, including the request associated with a response. Methods such as get or Fetch are rejected before signature construction or verification; the library does not change their case. This applies even when @method is not covered.
Individual query parameters use the component name @query-param with a name parameter, for example '"@query-param";name="Pet"'. The previous spelling @query-params is rejected.
Signatures require a created timestamp by default. To accept signatures without one, set verifier.require_created = False and pass max_age=None to verify(); an age limit cannot be enforced without a creation timestamp. Expiration checks still apply when expires is present.
If multiple signatures are found in the request but expect_tag=... was not passed, then InvalidSignature is raised. Also, the verify() method raises HTTPMessageSignaturesException or an exception derived from this class in case an error occurs (unable to load PEM key, unsupported algorithm specified in signature input, signature doesn’t match digest etc.)
Links
Bugs
Please report bugs, issues, feature requests, etc. on GitHub.
License
Copyright 2017-2024, Andrey Kislyuk and http-message-signatures contributors. Licensed under the terms of the Apache License, Version 2.0. Distribution of attribution information, LICENSE and NOTICE files with source copies of this package and derivative works is REQUIRED as specified by the Apache License.
Metadata
Release files for http-message-signatures 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| http_message_signatures-2.1.0.tar.gz | 53.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| http_message_signatures-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.3 kB
Release files / http_message_signatures-2.1.0.tar.gz
| Download URL | http_message_signatures-2.1.0.tar.gz |
|---|---|
| Size | 53.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
171183a16f17c0e668c42471d345aa1faca6ceb0bfda380fa2542c91ae6f8702
|
|
BLAKE2b-256 checksum How to use checksums |
2d44b1ddf9ed3fcf8d49df1dd83f23ce2ba7a6a7f0dd46a93ed801e0556939d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / http_message_signatures-2.1.0-py3-none-any.whl
| Download URL | http_message_signatures-2.1.0-py3-none-any.whl |
|---|---|
| Size | 27.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c54bacf9ef141864e51864e3a175f6cf777c405fb25bd4e003bfac6e8d9bb156
|
|
BLAKE2b-256 checksum How to use checksums |
1b887f5410f3b323258cee415eb473c33f96ddb3961874192d594cbe56804136
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for http-message-signatures 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for http_message_signatures-2.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.