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 requests
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()
signer = HTTPMessageSigner(signature_algorithm=algorithms.HMAC_SHA256, key_resolver=MyHTTPSignatureKeyResolver())
signer.sign(request, key_id="my-key", body=request.body)
verifier = HTTPMessageVerifier(signature_algorithm=algorithms.HMAC_SHA256, key_resolver=MyHTTPSignatureKeyResolver())
verifier.verify(request, body=request.body)
Signing Content-Digest and checking it against the body protects the body against tampering. Supply content bytes before decoding as body=. With this argument, sign() generates Content-Digest if absent and adds digest coverage to the signature, while verify() checks the signed digest against those bytes. When signing, components covered by the signature can be customized using covered_component_ids, for example covered_component_ids=("@method", "@authority", "@target-uri", "content-type", "content-digest").
Signing a message with an existing Content-Digest requires use_existing_digest=True on sign() or prepare(). With body= supplied, these methods validate the existing digest against those bytes and add digest coverage to the signature. Without body=, they check only the digest’s format and algorithms; you must ensure the digest matches the content and include it in covered_component_ids to sign it. Both methods preserve existing digest field lines and raise an error if the digest is malformed or does not match supplied content. They also raise an error if you supply use_existing_digest=True with neither a digest nor body bytes.
Integrations
Three integrations are available: Requests, HTTPX2, and aiohttp.
Requests
HTTPX2 integration
Install the optional dependency:
pip install 'http-message-signatures[httpx2]'
http_message_signatures.httpx2.SignatureHooks can sign prepared requests, verify responses, or do both through HTTPX2 event hooks. The default mode enables both operations. Configure each operation independently in sign_options and verify_options, including its origin, algorithm, and HTTPSignatureKeyResolver instance. The hooks pass each resolver directly to the base signer or verifier, which calls it to look up keys when processing a message. Signing and verification can use different algorithms and resolvers, or share one resolver. For example, these custom resolvers load keys on demand from application key stores:
import httpx2
from http_message_signatures import HTTPSignatureKeyResolver, algorithms
from http_message_signatures.httpx2 import SignatureHooks, SignatureHooksMode
class ClientKeyResolver(HTTPSignatureKeyResolver):
def resolve_private_key(self, key_id):
return signing_key_store.load_private_key(key_id)
class ServerKeyResolver(HTTPSignatureKeyResolver):
def resolve_public_key(self, key_id):
return trusted_key_store.load_public_key(key_id)
client_key_resolver = ClientKeyResolver()
server_key_resolver = ServerKeyResolver()
signatures = SignatureHooks(
sign_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_id": "client-1",
"key_resolver": client_key_resolver,
"tag": "example-api",
},
verify_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_resolver": server_key_resolver,
"expect_tag": "example-api",
"require_components": ("@status", '"@method";req', '"@target-uri";req'),
},
)
with httpx2.Client(event_hooks=signatures.event_hooks) as client:
response = client.post("https://api.example.com/items", json={"hello": "world"})
response.raise_for_status()
data = response.json()
results = response.extensions["http_message_signatures"]
If you just want to supply the keys directly, use http_message_signatures.MappingKeyResolver with private_keys and public_keys dictionaries keyed by key ID.
The server must return a signature whose key ID is trusted by the verification resolver, using the configured algorithm and application tag. This example configures the verifier to require status and request method/URL coverage. The hooks also require a signed digest of the response’s content if the response is nonempty or carries Content-Digest. A digest of request content selected using ;req does not satisfy this requirement. The verifier raises InvalidSignature for missing signatures, untrusted keys, insufficient configured coverage, or body mismatches before later response hooks or your code receive the response. The hooks verify signed error responses too; your application decides how to handle HTTP status codes.
Use the async hook collection with AsyncClient:
async with httpx2.AsyncClient(event_hooks=signatures.async_event_hooks) as client:
response = await client.post("https://api.example.com/items", json={"hello": "world"})
data = response.json()
Signing only or verification only
The first argument, mode, selects which hooks SignatureHooks includes in its hook lists. All other arguments are keyword-only:
SignatureHooksMode.SIGN_REQUEST_VERIFY_RESPONSE (the default) requires both sign_options and verify_options.
SignatureHooksMode.SIGN_REQUEST requires only sign_options. Responses use normal HTTPX2 handling.
SignatureHooksMode.VERIFY_RESPONSE requires only verify_options. Requests are sent without signing.
For signing only:
signatures = SignatureHooks(
SignatureHooksMode.SIGN_REQUEST,
sign_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_id": "client-1",
"key_resolver": client_key_resolver,
"tag": "example-api",
},
)
with httpx2.Client(event_hooks=signatures.event_hooks) as client:
response = client.post("https://api.example.com/items", json={"hello": "world"})
For verification only:
signatures = SignatureHooks(
SignatureHooksMode.VERIFY_RESPONSE,
verify_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_resolver": server_key_resolver,
"expect_tag": "example-api",
"require_components": ("@status",),
},
)
with httpx2.Client(event_hooks=signatures.event_hooks) as client:
response = client.get("https://api.example.com/items")
results = response.extensions["http_message_signatures"]
The same modes work with AsyncClient(event_hooks=signatures.async_event_hooks). Supply a sign_options dictionary when signing requests and a verify_options dictionary when verifying responses. Omit the dictionary for any disabled operation.
To accept a supplied request digest, set "use_existing_digest": True in sign_options. The hooks always validate it against the buffered request body. When retrying a request previously signed by the same hooks, the hooks inject this option automatically unless you explicitly set it to False. The hooks also allow digest reuse on redirects that retain the body. If you change the body, remove or replace its existing digest before signing again.
aiohttp integration
Install the optional dependency (aiohttp 3.14 or later):
pip install 'http-message-signatures[aiohttp]'
http_message_signatures.aiohttp.SignatureMiddleware signs requests and verifies responses through aiohttp.ClientSession. Its sign_options and verify_options dictionaries accept custom key resolvers and independent algorithms. Pass session_kwargs to the session constructor:
import aiohttp
from http_message_signatures.aiohttp import SignatureMiddleware, SignatureMiddlewareMode
signatures = SignatureMiddleware(
sign_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_id": "client-1",
"key_resolver": client_key_resolver,
"tag": "example-api",
},
verify_options={
"origin": "https://api.example.com",
"signature_algorithm": algorithms.ED25519,
"key_resolver": server_key_resolver,
"expect_tag": "example-api",
"require_components": ("@status", '"@method";req', '"@target-uri";req'),
},
)
async with aiohttp.ClientSession(**signatures.session_kwargs) as client:
async with client.post("https://api.example.com/items", json={"hello": "world"}, allow_redirects=False) as response:
response.raise_for_status()
data = await response.json()
results = signatures.verification_results(response)
The first constructor argument selects the mode. SignatureMiddlewareMode.SIGN_REQUEST needs only sign_options; SignatureMiddlewareMode.VERIFY_RESPONSE needs only verify_options. The default, SignatureMiddlewareMode.SIGN_REQUEST_VERIFY_RESPONSE, needs both dictionaries. The middleware forwards supplied signature options to the base signer and verifier; those methods provide defaults for omitted options.
Signing buffers the upload, generates Content-Digest, and signs the prepared request. aiohttp’s compress=True and compress="gzip" options work: the middleware uses aiohttp’s compressor before signing. Retries and redirects reuse buffered content without recompressing it. To supply precompressed content yourself, send encoded bytes with Content-Encoding. Existing digests require "use_existing_digest": True in sign_options and must match the encoded body. The middleware permits reuse on retries of the same request unless you explicitly set this option to False.
Verification checks the encoded response before decoding it for read(), text(), json(), and content. VerifyResult.body contains the encoded bytes. Client buffering has no size limit, retains encoded and decoded content in memory, and discards HTTP chunk boundaries. Streaming readers do not bound this memory use. Decoded replay uses aiohttp’s internal payload parser and stream APIs. Signing-only mode leaves response handling to aiohttp.
Place the signature middleware last, so other middleware can change requests before signing and read responses after verification. Keep auto_decompress=False from session_kwargs and retain the signature middleware when overriding the per-request middleware list. Setting auto_decompress=True can expose unverified content to aiohttp’s decoder before the middleware rejects it.
Requests and redirects must match each enabled operation’s configured origin. Keep allow_redirects=False unless your verification policy requires signed Location on redirect responses: an origin check does not authenticate paths within that origin. Require coverage of other fields the application trusts, including Content-Type and Content-Encoding. Failed verification prevents cookie-jar updates; authenticating cookies also requires '"set-cookie";bs' coverage.
aiohttp servers
http_message_signatures.aiohttp.ServerSignatureMiddleware verifies requests and signs responses through aiohttp.web.Application. ServerVerificationOptions and ServerSigningOptions accept independent algorithms and key resolvers, without an origin. Applications control accepted hosts, trusted proxies, and authorization.
This example uses the client and server key resolvers defined earlier:
from aiohttp import web
from http_message_signatures import InvalidSignature, algorithms
from http_message_signatures.aiohttp import ServerSignatureMiddleware
signatures = ServerSignatureMiddleware(
verify_options={
"signature_algorithm": algorithms.ED25519,
"key_resolver": client_key_resolver,
"expect_tag": "example-api",
"require_components": ("@method", "@target-uri"),
},
sign_options={
"signature_algorithm": algorithms.ED25519,
"key_id": "server-1",
"key_resolver": server_key_resolver,
"tag": "example-api",
"covered_component_ids": (
"@status", "content-type", '"@method";req', '"@target-uri";req',
),
},
)
@web.middleware
async def signature_errors(request, handler):
try:
return await handler(request)
except InvalidSignature:
raise web.HTTPForbidden(text="Invalid request signature") from None
async def echo(request):
results = signatures.verification_results(request)
# Apply your application's authorization policy using these verified results.
return web.json_response(await request.json())
app = web.Application(**signatures.application_kwargs)
app.middlewares.insert(0, signature_errors)
app.router.add_post("/items", echo)
app.on_response_prepare.append(signatures.on_response_prepare)
web.run_app(app)
The first argument selects ServerSignatureMiddlewareMode.VERIFY_REQUEST, SIGN_RESPONSE, or VERIFY_REQUEST_SIGN_RESPONSE (the default). Supply options for the enabled operations. Verification-only mode needs no preparation callback; signing-only mode leaves request bodies to the handler. The base signer and verifier supply omitted defaults. Configure response covered_component_ids explicitly because the base defaults cover requests; the middleware adds Content-Digest coverage.
application_kwargs installs the middleware and, when verifying, sets handler_args={"auto_decompress": False}. Keep that setting when adding other options. The middleware verifies headers and encoded content before decoding or invoking the handler. Both encoded and decoded buffers respect request.client_max_size; zero disables the limit. Handlers can use read(), text(), json(), post(), multipart(), and content normally. verification_results(request) returns successful results with encoded content in VerifyResult.body.
Place trusted proxy middleware before signature verification and body consumers after it. The middleware captures the effective request URL and headers for ;req response components; it does not trust Forwarded headers automatically. Applications handle InvalidSignature (403 in this example), authorization, and replay prevention. Require coverage of fields the application trusts, such as Content-Type, Content-Encoding, Cookie, Set-Cookie, or Location.
For signing, return a web.Response with bytes, text, JSON, or a finite Payload. The middleware buffers and closes Payloads before preparation. aiohttp applies enable_compression() normally; the preparation callback signs encoded content and finalized headers, including cookies. HEAD, 204, and 304 responses have empty content digests. Existing digests require "use_existing_digest": True in sign_options and must match the transmitted content.
Register on_response_prepare last, after callbacks that modify headers. For subapplications, the callback must run after child callbacks too; register it once for each route’s application chain, on the relevant leaf application. The middleware checks this order before invoking the handler. Place the signature middleware outside middleware that changes response bodies, and do not change bodies or covered headers after preparation. Application error responses also pass through the signing callback; a response signature does not imply that its request was verified. If signing fails, the adapter closes the connection before sending response headers.
Bytes-backed responses support explicit prepare() calls. Return Payload responses without preparing them yourself so the middleware can buffer them. Automatic signing does not support StreamResponse, SSE, FileResponse, WebSockets, or chunked responses with compression; use a buffered web.Response instead. An explicit Content-Length must match the final bytes body. When buffering a Payload, the middleware honors its declared Content-Length limit. Response buffering has no size limit; supply finite bodies that fit in memory. Request replay uses aiohttp’s private payload and read-cache attributes, and response signing reads its private compressed-body cache.
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. When you supply body=, it generates Content-Digest if absent and adds digest coverage to the signature. 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.
In this example, request is a prepared Requests request whose body contains encoded content bytes and whose headers do not yet contain Content-Digest. Passing body=request.body makes prepare() generate the digest and add it to the covered components before building the signing input.
The asynchronous sign_base function signs the supplied bytes with Ed25519 and returns the raw 64-byte signature. The external signer 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,
body=request.body,
label="sig1",
)
signature = await sign_base(prepared.signature_base)
signer.apply(request, prepared, signature)
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.
External signing services differ in their accepted input and output formats. When calling a signing service, configure its signing operation according to RFC 9421 section 3.3. Ed25519 signs the base without prehashing. For RSA and ECDSA, an API that accepts raw messages must perform the specified hashing. If the API accepts a digest, hash the signature base before calling it and select a mode that does not hash the digest again. 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().
Structured Field serialization
Use the sf component parameter to cover a whole header field using Structured Fields serialization:
signer.sign(request, key_id="my-key", covered_component_ids=('"priority";sf',))
For example, Priority: u=03, i=?1 produces this signature-base line:
"priority";sf: u=3, i
The field’s Item, List, or Dictionary type is selected by its name using http_sf’s registry. Both endpoints must use matching type definitions. The registry may omit otherwise standardized fields; an unknown type or a value invalid for its registered type raises HTTPMessageSignaturesException during signing, or InvalidSignature during verification. The library does not infer types from field values. Headers covered without sf use the ordinary HTTP field normalization rules and do not require a registry entry.
The registry also contains retrofit mappings for legacy headers whose parsing rules can differ from Structured Fields. For example, this header:
Cache-Control: max-age=3600
can be changed to:
Cache-Control: max-age=0, max-age=3600
Both produce the same signed component because Structured Fields Dictionary parsing keeps the last duplicate:
"cache-control";sf: max-age=3600
However, RFC 9111 section 4.2.1 says duplicate freshness directives should use the first occurrence or make the response stale. The change can therefore alter caching behavior without invalidating a signature that covers "cache-control";sf. The retrofit draft acknowledges this difference in duplicate handling.
Omit ;sf when the application’s or other HTTP consumers’ interpretation differs from Structured Fields semantics, or compatibility is unknown. For example, use covered_component_ids=("cache-control",) to keep both duplicate directives in the signed value; adding one then changes the signature base. Use ;sf for fields defined as Structured Fields, or legacy fields only when compatible interpretation is enforced. Applications must still validate header values: successful signature verification does not establish their semantic validity.
Repeated field lines are combined before parsing. In a response signature, '"priority";req;sf' uses the associated request’s field. Dictionary-member selection with key continues to work without a registry entry and may include the redundant sf flag. The tr parameter remains unsupported.
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 2022-2026, 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 3.0.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 | Actions |
|---|---|---|---|---|
| http_message_signatures-3.0.0.tar.gz | 116.0 kB | With provenance | Details for http_message_signatures-3.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| http_message_signatures-3.0.0-py3-none-any.whl | Python 3 | none | any | Details for http_message_signatures-3.0.0-py3-none-any.whl |
Total release size: 169.4 kB
Release files / http_message_signatures-3.0.0.tar.gz
| Download URL | http_message_signatures-3.0.0.tar.gz |
|---|---|
| Size | 116.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fcb26b157462483b4d6b264df8c9c1a796439a78a3d42ad412bcbf507f39db01
|
|
BLAKE2b-256 checksum How to use checksums |
77c7eecd8e93818a29f6ceb02145018ee7c36469da543af7410981d2e9d40ee5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 26, 2026.
Transparency logRelease files / http_message_signatures-3.0.0-py3-none-any.whl
| Download URL | http_message_signatures-3.0.0-py3-none-any.whl |
|---|---|
| Size | 53.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c289966e1b2950517cd57aed166be5e981de96f0cd29f69173f09ac547a8552a
|
|
BLAKE2b-256 checksum How to use checksums |
14e7fc0c602d1e921b13e8451a9287289594871ca3c448f0c4d9bf0a9bd3b0b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 26, 2026.
Transparency logSecurity for http-message-signatures 3.0.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-3.0.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 26, 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.