requests-http-signature: A Requests auth module for HTTP Signature

requests-http-signature is a Requests authentication plugin (requests.auth.AuthBase subclass) implementing the IETF RFC 9421 HTTP Message Signatures standard.

Installation

$ pip install requests-http-signature

Usage

import requests
from http_message_signatures import HTTPSignatureKeyResolver
from requests_http_signature import HTTPSignatureAuth, algorithms

preshared_key_id = 'squirrel'
preshared_secret = b'monorail_cat'
url = 'https://example.com/'

auth = HTTPSignatureAuth(key=preshared_secret,
                         key_id=preshared_key_id,
                         signature_algorithm=algorithms.HMAC_SHA256)
requests.get(url, auth=auth, allow_redirects=False)

By default, the Date header and the @method, @authority, and @target-uri derived components are signed. The Date header is set if it is absent, and Authorization is signed if present. The signer generates and signs a SHA-256 Content-Digest for buffered request content, including empty content on GET requests, using the format described in RFC 9530, Digest Fields. To add other headers to the signature, pass an array of header names in the covered_component_ids keyword argument. See the API documentation for the full list of options and details.

Verifying responses

The class method HTTPSignatureAuth.verify() can be used to verify responses received back from the server:

class MyKeyResolver(HTTPSignatureKeyResolver):
    def resolve_public_key(self, key_id):
        assert key_id == 'squirrel'
        return b'monorail_cat'

with requests.get(url, auth=auth, stream=True, allow_redirects=False) as response:
    verify_result = HTTPSignatureAuth.verify(response,
                                            signature_algorithm=algorithms.HMAC_SHA256,
                                            key_resolver=MyKeyResolver())

Response verification requires @status plus "@method";req, "@authority";req, and "@target-uri";req by default. The req flag binds the response to its associated request. The server must sign these components; bare request-derived components are invalid in response signatures. If your protocol uses a different coverage profile, pass it explicitly as require_components.

verify() reads and verifies the response body automatically. Use stream=True as shown above so it can verify compressed content before Requests decodes it. After verification, use response.content, response.text, or response.json() normally. verify_result.body contains the authenticated bytes before content decoding. Verification buffers content in memory; it does not provide incremental verification for arbitrarily large downloads.

Nonempty content requires a covered digest. Empty responses, including HEAD, 204, and 304, do not require a digest unless it is explicitly required or present. When present, a covered digest of empty content is verified as well.

More generally, you can reconstruct an arbitrary request using the Requests API and pass it to verify():

request = requests.Request(...)  # Reconstruct the incoming request using the Requests API
prepared_request = request.prepare()  # Generate a PreparedRequest
HTTPSignatureAuth.verify(prepared_request, ...)

To verify incoming requests and sign responses in the context of an HTTP server, see the flask-http-signature and http-message-signatures packages.

See what is signed

It is important to understand and follow the best practice rule of “See what is signed” when verifying HTTP message signatures. The gist of this rule is: if your application neglects to verify that the information it trusts is what was actually signed, the attacker can supply a valid signature but point you to malicious data that wasn’t signed by that signature. Failure to follow this rule can lead to vulnerability against signature wrapping and substitution attacks.

In requests-http-signature, you can ensure that the information signed is what you expect to be signed by only trusting the data returned by the verify() method:

verify_result = HTTPSignatureAuth.verify(message, ...)

See the API documentation for full details.

Asymmetric key algorithms

To sign or verify messages with an asymmetric key algorithm, set the signature_algorithm keyword argument to algorithms.ED25519, algorithms.ECDSA_P256_SHA256, algorithms.RSA_V1_5_SHA256, or algorithms.RSA_PSS_SHA512.

For asymmetric key algorithms, you can supply the private key as the key parameter to the HTTPSignatureAuth() constructor as bytes in the PEM format, or configure the key resolver as follows:

with open('key.pem', 'rb') as fh:
    auth = HTTPSignatureAuth(signature_algorithm=algorithms.RSA_V1_5_SHA256,
                             key=fh.read(),
                             key_id=preshared_key_id)
requests.get(url, auth=auth, allow_redirects=False)

class MyKeyResolver(HTTPSignatureKeyResolver):
    def resolve_public_key(self, key_id: str):
        return public_key_pem_bytes[key_id]

    def resolve_private_key(self, key_id: str):
        return private_key_pem_bytes[key_id]

auth = HTTPSignatureAuth(signature_algorithm=algorithms.RSA_V1_5_SHA256,
                         key_resolver=MyKeyResolver(),
                         key_id="my-key-id")
requests.get(url, auth=auth, allow_redirects=False)

Digest algorithms

Digest generation and validation use the shared http_message_signatures.ContentDigest implementation. To generate SHA-512 instead of the default SHA-256, configure a signer subclass:

from http_message_signatures import ContentDigest, HTTPMessageSigner

class SHA512Digest(ContentDigest):
    algorithm = "sha-512"

class SHA512Signer(HTTPMessageSigner):
    content_digest_class = SHA512Digest

class MyAuth(HTTPSignatureAuth):
    signer_class = SHA512Signer

signer_class replaces the former signing_content_digest_algorithm and add_digest() customization points. Fixed keys use the upstream MappingKeyResolver; the package no longer provides SingleKeyResolver. Signing and verification errors from the shared APIs propagate as HTTPMessageSignaturesException or its InvalidSignature subclass. Requests-specific preparation errors raise RequestsHttpSignatureException.

Request bodies and streams

Bytes, text, form dictionaries, JSON, and Requests’ buffered multipart uploads are supported. Text is encoded as UTF-8 before hashing and sending. A supplied Content-Digest requires use_existing_digest=True on the auth object and is checked against buffered content before signing. This option is also required when signing a request again or appending another signature. A stale digest is rejected; remove or replace it after changing the content.

File objects and iterators require a caller-supplied Content-Digest for the bytes that will be sent from their current position and use_existing_digest=True. The auth handler validates the supplied digest’s format and algorithms and signs the whole field without reading or rewinding the stream. For example:

import io
from http_message_signatures import ContentDigest

payload = b"upload content"
digest = ContentDigest.create(payload)
stream_auth = HTTPSignatureAuth(key=preshared_secret, key_id=preshared_key_id,
                                signature_algorithm=algorithms.HMAC_SHA256, use_existing_digest=True)
response = requests.post(url, data=io.BytesIO(payload), headers={"Content-Digest": digest},
                         auth=stream_auth, allow_redirects=False)

Keep a streaming body unchanged after computing its digest. Verification reads seekable binary request bodies from the starting position recorded by Requests and restores their current position afterward. Non-seekable request bodies cannot be verified. Unsupported streams without a supplied digest raise RequestsHttpSignatureException. Malformed, duplicate, wrongly typed, unsupported, or mismatched digests during verification raise InvalidSignature.

Component identifiers

Use lowercase component names. Individual query parameters use @query-param with a name parameter:

components = ('"@query-param";name="Foo"', '"@query-param";name="foo"')
query_auth = HTTPSignatureAuth(key_id=preshared_key_id, key=preshared_secret,
                         signature_algorithm=algorithms.HMAC_SHA256,
                         covered_component_ids=components)

The two query names above are distinct and both are signed. Component identity includes parameter values but ignores parameter order, as specified in RFC 9421 section 2. Duplicate identifiers, including date and "date", are errors. When a supplied header container exposes names more than once under case-insensitive matching, the package raises an error instead of choosing a value. It cannot recover field lines already discarded by a header container.

Dictionary-member coverage such as '"content-digest";key="sha-256";sf' is supported. Its parameter order may differ in require_components; the signed wire order is preserved. A covered digest member suffices for body validation without requiring the whole Content-Digest field. @request-response and unsupported Structured Field flags are not implemented.

External signing

Use prepare() and apply() on a fully prepared Requests request when a remote service holds the private key. No local key or resolver is needed. The auth object prepares the body digest, Date, covered components, nonce, and creation/expiration parameters, then returns a PreparedSignature:

session = requests.Session()
external_auth = HTTPSignatureAuth(key_id="remote-ed25519-key", signature_algorithm=algorithms.ED25519, tag="my-app")
request = session.prepare_request(requests.Request("POST", url, json={"hello": "world"}))
prepared = external_auth.prepare(request)
signature = sign_base(prepared.signature_base)  # Your service signs these bytes with Ed25519.
external_auth.apply(request, prepared, signature)
response = session.send(request, allow_redirects=False)

For an asynchronous signer, await sign_base(prepared.signature_base) between preparation and application. Requests’ send operation remains synchronous. Do not pass a keyless auth object through auth=: that path performs local signing. Use a session without another request-signing auth handler when preparing the request externally.

apply() accepts raw signature bytes, checks covered headers and buffered body digests for changes, and returns the request. It does not authenticate the external service, verify its signature, refresh timestamps, or validate a stream against a supplied digest. Keep the prepared object associated with the right request and keep that request stable until sending. For DER-encoded ECDSA output, use algorithms.ECDSA_P256_SHA256.signature_from_der() before applying it. See the http-message-signatures external-signing documentation for algorithm input requirements.

Signature selection and verification policies

Set a signed application tag when signing. To preserve another signature, also choose a distinct label and set append_if_signature_exists=True on the auth object or its apply() call. During verification:

result = HTTPSignatureAuth.verify(message, signature_algorithm=algorithms.HMAC_SHA256,
                                  key_resolver=MyKeyResolver(), expect_tag="my-app", expect_label="sig1")

Selection must leave one verified signature. Labels are not signed, so the default policy requires expect_tag when expect_label is used. Use result.nonce to enforce your application’s replay policy.

max_age=None disables the age limit but retains expiration checks. Customize the existing verifier class to change creation-time requirements or clock skew:

import datetime
from http_message_signatures import HTTPMessageVerifier

class MyVerifier(HTTPMessageVerifier):
    require_created = False
    max_clock_skew = datetime.timedelta(seconds=30)

class MyAuth(HTTPSignatureAuth):
    verifier_class = MyVerifier

result = MyAuth.verify(message, signature_algorithm=algorithms.HMAC_SHA256,
                       key_resolver=MyKeyResolver(), max_age=None)

Redirects

Pass allow_redirects=False for signed requests. Requests copies signature headers while following redirects and does not re-run custom auth handlers; changing the URL or method invalidates the signature. Automatic redirect re-signing is not provided. Decide whether to follow each destination, then prepare and sign a fresh request. For example, this GET-only flow permits one redirect within the original origin:

from urllib.parse import urljoin, urlsplit

response = requests.get(url, auth=auth, allow_redirects=False)
if response.is_redirect:
    destination = urljoin(response.url, response.headers["Location"])
    origin, target = urlsplit(url), urlsplit(destination)
    response.close()
    if (origin.scheme, origin.netloc) != (target.scheme, target.netloc):
        raise ValueError("Redirect destination is not permitted")
    response = requests.get(destination, auth=auth, allow_redirects=False)

For POST or streamed uploads, handle method changes and replay of the body explicitly. Do not reuse stale digest or signature headers from the first request.

Authors

  • Andrey Kislyuk <https://kislyuk.com>

License

Copyright 2017-2026, Andrey Kislyuk and requests-http-signature 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.

API documentation

exception requests_http_signature.RequestsHttpSignatureException(*args, **kwargs)[source]

An error occurred while constructing the HTTP Signature for your request.

Initialize RequestException with request and response objects.

Parameters:
  • args (Any)

  • kwargs (Any)

Return type:

None

class requests_http_signature.HTTPSignatureAuth(*, signature_algorithm, key=None, key_id, key_resolver=None, covered_component_ids=('@method', '@authority', '@target-uri'), label=None, tag=None, append_if_signature_exists=False, include_alg=True, use_nonce=False, expires_in=None, use_existing_digest=False)[source]

A Requests authentication plugin (requests.auth.AuthBase subclass) implementing RFC 9421, HTTP Message Signatures.

Parameters:
  • signature_algorithm (type[HTTPSignatureAlgorithm]) – One of requests_http_signature.algorithms.HMAC_SHA256, requests_http_signature.algorithms.ECDSA_P256_SHA256, requests_http_signature.algorithms.ED25519, requests_http_signature.algorithms.RSA_PSS_SHA512, or requests_http_signature.algorithms.RSA_V1_5_SHA256.

  • key (bytes | None) – Key material that will be used to sign the request. In the case of HMAC, this should be the raw bytes of the shared secret; for all other algorithms, this should be the bytes of the PEM-encoded private key material.

  • key_id (str) – The key ID to use in the signature.

  • key_resolver (HTTPSignatureKeyResolver | None) – Instead of specifying a fixed key, you can instead pass a key resolver, which should be an instance of a subclass of http_message_signatures.HTTPSignatureKeyResolver. A key resolver should have two methods, resolve_private_key(key_id) (required only for signing) and resolve_public_key(key_id) (required only for verifying). Your implementation should ensure that the key id is recognized and return the corresponding key material as PEM bytes, cryptography key objects, or shared secret bytes for HMAC. Omit both key and key_resolver when using prepare() and apply() with an external signer.

  • covered_component_ids (Sequence[str]) – Lowercase header names or serialized RFC 9421 component identifiers. Request derived components include @method, @target-uri, @authority, @scheme, @request-target, @path, @query, and '"@query-param";name="Pet"'. Parameter values are case-sensitive; their order does not affect identity. Duplicate component identifiers are rejected. Defaults to @method, @authority, and @target-uri. Authorization and Date are also covered when present. The signer adds digest coverage for buffered content, including empty requests. Explicit digest-member coverage, such as '"content-digest";key="sha-256"', replaces automatic whole-field coverage for buffered content. Streams always cover the whole supplied Content-Digest field.

  • label (str | None) – The signature dictionary label, which is not signed.

  • tag (str | None) – A signed application tag, usable with verify(expect_tag=...).

  • append_if_signature_exists (bool) – Append a distinct signature label instead of replacing existing signatures.

  • include_alg (bool) – By default, the signature parameters will include the alg parameter, using it to identify the signature algorithm. If you wish not to include this parameter, set this to False.

  • use_nonce (bool) – Set this to True to include a unique message-specific nonce in the signature parameters. The format of the nonce can be controlled by subclassing this class and overloading the get_nonce() method.

  • expires_in (timedelta | None) – Use this to set the expires signature parameter to the time of signing plus the given timedelta.

  • use_existing_digest (bool) – Allow a supplied Content-Digest, validating it against buffered bytes before signing. Required for streams with precomputed digests and when signing a request again.

component_resolver_class

A subclass of http_message_signatures.HTTPSignatureComponentResolver can be used to override this value to customize the retrieval of header and derived component values if needed.

alias of HTTPSignatureComponentResolver

verifier_class

A verifier subclass can configure creation-time requirements and clock-skew tolerance.

alias of HTTPMessageVerifier

signer_class

A signer subclass can select a ContentDigest subclass through its content_digest_class attribute.

alias of HTTPMessageSigner

prepare(request)[source]

Set digest/date headers and return signing input without resolving a key.

Use a fully prepared Requests request. Keep the returned object with this request while an external signer signs its signature_base bytes.

Parameters:

request (PreparedRequest)

Return type:

PreparedSignature

apply(request, prepared, signature, *, append_if_signature_exists=None)[source]

Attach raw signature bytes and return the request ready for Session.send.

Covered values must still match preparation. Buffered bodies are checked against their digest; callers supplying stream digests must keep the stream unchanged. This does not verify the external signature or refresh its age.

Parameters:
  • request (PreparedRequest)

  • prepared (PreparedSignature)

  • signature (bytes)

  • append_if_signature_exists (bool | None)

Return type:

PreparedRequest

classmethod get_body(message)[source]

Read content for digest verification, preserving buffered responses and seekable request bodies.

Compressed responses must be requested with stream=True. Their encoded content is buffered internally and remains available to Requests for normal decoding. Seekable binary request bodies are read from the starting position recorded by Requests, then restored to their current position.

Parameters:

message (PreparedRequest | Response)

Return type:

bytes

classmethod verify(message, *, require_components=None, signature_algorithm, key_resolver, max_age=datetime.timedelta(days=1), expect_tag=None, expect_label=None)[source]

Verify an HTTP message signature and its body digest using content read from the message.

Request compressed responses with stream=True so this method can read their encoded content. It buffers that content internally and preserves Requests’ normal content, text, and json() access after verification. Already decoded or partially consumed response streams cannot be verified. Buffered requests and seekable binary request bodies are read automatically; seekable bodies retain their current position. Missing unencoded response content is treated as empty bytes.

See what is signed

It is important to understand and follow the best practice rule of “See what is signed” when verifying HTTP message signatures. The gist of this rule is: if your application neglects to verify that the information it trusts is what was actually signed, the attacker can supply a valid signature but point you to malicious data that wasn’t signed by that signature. Failure to follow this rule can lead to vulnerability against signature wrapping and substitution attacks.

You can ensure that the information signed is what you expect to be signed by only trusting the VerifyResult tuple returned by verify().

Parameters:
  • message (PreparedRequest | Response) –

    The HTTP response or request to verify. You can either pass a received response, or reconstruct an arbitrary request using the Requests API:

    request = requests.Request(...)
    prepared_request = request.prepare()
    HTTPSignatureAuth.verify(prepared_request, ...)
    

  • require_components (Sequence[str] | None) –

    Lowercase header names or serialized component identifiers whose coverage is required. Parameter order is ignored for comparison, but parameter values retain their case and types. Duplicate identifiers fail. The request default is ("@method", "@authority", "@target-uri"). The response default is ("@status", '"@method";req', '"@authority";req', '"@target-uri";req'), binding the response to its request. Supply a different profile explicitly if your peer signs other components.

    Nonempty content, or a present Content-Digest field, additionally requires a covered content digest. A selected digest dictionary member is sufficient. Empty messages do not require an absent digest; any present digest is still verified, including the digest of empty content. Request components with ;req do not satisfy coverage of the response body. Validation returns the bytes in result.body.

  • signature_algorithm (type[HTTPSignatureAlgorithm]) – The algorithm expected to be used by the signature. Any signature not using the expected algorithm will cause an InvalidSignature exception. Must be one of requests_http_signature.algorithms.HMAC_SHA256, requests_http_signature.algorithms.ECDSA_P256_SHA256, requests_http_signature.algorithms.ED25519, requests_http_signature.algorithms.RSA_PSS_SHA512, or requests_http_signature.algorithms.RSA_V1_5_SHA256.

  • key_resolver (HTTPSignatureKeyResolver) – A key resolver, which should be an instance of a subclass of http_message_signatures.HTTPSignatureKeyResolver. A key resolver should have two methods, resolve_private_key(key_id) (required only for signing) and resolve_public_key(key_id) (required only for verifying). Your implementation should ensure that the key id is recognized and return the corresponding key material as PEM bytes (or shared secret bytes for HMAC).

  • max_age (timedelta | None) – Maximum age since created; None disables this limit, but expiration is still checked. Customize verifier_class to change its require_created and max_clock_skew policies.

  • expect_tag (str | None) – Select a signed application tag. Selection must leave one verified signature.

  • expect_label (str | None) – Further select the dictionary label; requires expect_tag under the default policy.

Returns:

VerifyResult, a namedtuple with the following attributes:

  • label (str): The label for the signature

  • algorithm: (same as signature_algorithm above)

  • covered_components: A mapping of component names to their values, as covered by the signature

  • parameters: A mapping of signature parameters to their values, as covered by the signature, including “alg”, “created”, “expires”, “keyid”, and “nonce”. To protect against replay attacks, retrieve the “nonce” parameter here and check that it has not been seen before.

  • nonce: The signed nonce, or None. Applications must enforce replay detection.

  • body: Content bytes before decoding, after validating their covered digest; None otherwise.

Raises:
  • InvalidSignature – Signature, required coverage, or body-digest validation fails.

  • HTTPMessageSignaturesException – The algorithm, required components, or selection policy is invalid.

Return type:

VerifyResult