Source code for http_message_signatures.structures
import re
from collections import OrderedDict
from collections.abc import Mapping, MutableMapping
from dataclasses import dataclass
from typing import NamedTuple
from .algorithms import HTTPSignatureAlgorithm
[docs]
@dataclass(frozen=True)
class PreparedSignature:
"""Immutable signing input returned by ``HTTPMessageSigner.prepare()``.
:ivar signature_base: ASCII bytes to pass to the signing algorithm, without
additional encoding or an added newline.
:ivar signature_params: Serialized ``@signature-params`` value, including the
covered component list and signed metadata.
:ivar label: Signature dictionary label (not covered by the signature).
:ivar algorithm_id: Configured algorithm ID, retained even when ``alg`` is omitted.
Retain this object until applying the corresponding signature. It contains
covered message values, which may be sensitive. Immutability prevents
accidental edits; it does not authenticate data sent through a queue or service.
"""
signature_base: bytes
signature_params: str
label: str
algorithm_id: str
[docs]
class VerifyResult(NamedTuple):
"""A verified signature and the values it covers.
:ivar label: Signature dictionary label (not covered by the signature).
:ivar algorithm: Algorithm class used for verification.
:ivar covered_components: Serialized component identifiers mapped to signed
values, including the generated ``\"@signature-params\"`` entry.
:ivar parameters: Signed signature metadata, such as ``keyid`` and ``created``.
:ivar body: ``None`` in this package; reserved for integrations validating body digests.
:ivar nonce: Signed nonce, or ``None`` when omitted.
"""
label: str
algorithm: type[HTTPSignatureAlgorithm]
covered_components: dict[str, str]
parameters: dict[str, int | str]
body: bytes | None
nonce: str | None
def normalize_header_value(value: str) -> str:
# RFC 9421 section 2.1: unfold obsolete HTTP/1.1 line folding and trim OWS.
# Bare CR/LF are not folds and must remain visible to validation.
# Only try preceding OWS at the start of a whitespace run, avoiding
# quadratic retries. CRLF can still match after a previous fold's RWS.
return re.sub(r"(?:(?<![ \t])[ \t]+)?\r\n[ \t]+", " ", value).strip(" \t")
class CaseInsensitiveDict(MutableMapping):
"""
A copy of requests.structures.CaseInsensitiveDict.
"""
def __init__(self, data=None, **kwargs):
self._store = OrderedDict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
def __eq__(self, other):
if isinstance(other, Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
def get_header_fields(headers):
"""Combine all field lines exposed by items(), preserving their wire order."""
fields = CaseInsensitiveDict()
for name, value in headers.items():
value = normalize_header_value(value)
if name in fields:
fields[name] += ", " + value
else:
fields[name] = value
return fields