182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
import warnings
|
|
from typing import Any
|
|
from collections.abc import Collection
|
|
from enum import Enum
|
|
from .model import JWSAlgModel
|
|
from ..errors import (
|
|
JoseError,
|
|
UnsupportedAlgorithmError,
|
|
SecurityWarning,
|
|
ExceededSizeError,
|
|
)
|
|
from ..registry import (
|
|
JWS_HEADER_REGISTRY,
|
|
Header,
|
|
HeaderRegistryDict,
|
|
check_registry_header,
|
|
check_crit_header,
|
|
check_supported_header,
|
|
)
|
|
from .._keys import KeySet
|
|
|
|
__all__ = [
|
|
"JWSRegistry",
|
|
"construct_registry",
|
|
"default_registry",
|
|
]
|
|
|
|
|
|
class JWSRegistry:
|
|
"""A registry for JSON Web Signature to keep all the supported algorithms.
|
|
An instance of ``JWSRegistry`` is usually used together with methods in
|
|
``joserfc.jws``.
|
|
|
|
:param header_registry: extra header parameters registry
|
|
:param algorithms: allowed algorithms to be used
|
|
:param strict_check_header: only allow header key in the registry to be used
|
|
"""
|
|
|
|
class Strategy(Enum):
|
|
#: find the recommended algorithm
|
|
RECOMMENDED = 1
|
|
#: find the most secure algorithm
|
|
SECURITY = 2
|
|
|
|
default_header_registry: HeaderRegistryDict = JWS_HEADER_REGISTRY
|
|
algorithms: dict[str, JWSAlgModel] = {}
|
|
recommended: list[str] = []
|
|
|
|
#: max header content's size in bytes
|
|
max_header_length: int = 512
|
|
#: max payload content's size in bytes
|
|
max_payload_length: int = 128000
|
|
#: max signature's size in bytes
|
|
max_signature_length: int = 1024
|
|
|
|
def __init__(
|
|
self,
|
|
header_registry: HeaderRegistryDict | None = None,
|
|
algorithms: Collection[str] | None = None,
|
|
strict_check_header: bool = True,
|
|
):
|
|
self.header_registry: HeaderRegistryDict = {}
|
|
self.header_registry.update(self.default_header_registry)
|
|
if header_registry is not None:
|
|
self.header_registry.update(header_registry)
|
|
self.allowed = algorithms
|
|
self.strict_check_header = strict_check_header
|
|
|
|
@classmethod
|
|
def register(cls, alg: JWSAlgModel) -> None:
|
|
"""Register a given JWS algorithm instance to the registry."""
|
|
cls.algorithms[alg.name] = alg
|
|
if alg.recommended:
|
|
cls.recommended.append(alg.name)
|
|
|
|
def get_alg(self, name: str) -> JWSAlgModel:
|
|
"""Get the allowed algorithm instance of the given name.
|
|
|
|
:param name: value of the ``alg``, e.g. ``HS256``, ``RS256``
|
|
"""
|
|
if name not in self.algorithms:
|
|
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not supported")
|
|
|
|
if self.allowed:
|
|
if name not in self.allowed:
|
|
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not allowed")
|
|
else:
|
|
if name not in self.recommended:
|
|
raise UnsupportedAlgorithmError(f"Algorithm of '{name}' is not recommended")
|
|
|
|
alg = self.algorithms[name]
|
|
if alg.security_warning:
|
|
warnings.warn(alg.security_warning, SecurityWarning)
|
|
return alg
|
|
|
|
def check_header(self, header: Header) -> None:
|
|
"""Check and validate the fields in header part of a JWS object."""
|
|
check_crit_header(self.header_registry, header)
|
|
check_registry_header(self.header_registry, header)
|
|
if self.strict_check_header:
|
|
check_supported_header(self.header_registry, header)
|
|
|
|
def validate_header_size(self, header: bytes) -> None:
|
|
if header and len(header) > self.max_header_length:
|
|
raise ExceededSizeError(f"Header size exceeds {self.max_header_length} bytes.")
|
|
|
|
def validate_payload_size(self, payload: bytes) -> None:
|
|
if payload and len(payload) > self.max_payload_length:
|
|
raise ExceededSizeError(f"Payload size exceeds {self.max_payload_length} bytes.")
|
|
|
|
def validate_signature_size(self, signature: bytes) -> None:
|
|
if len(signature) > self.max_signature_length:
|
|
raise ExceededSizeError(f"Signature of exceeds {self.max_signature_length} bytes.")
|
|
|
|
@classmethod
|
|
def guess_algorithm(cls, key: Any, strategy: Strategy) -> JWSAlgModel | None:
|
|
"""Guess the JWS algorithm for a given key.
|
|
|
|
:param key: key instance or a KeySet
|
|
:param strategy: the strategy for guessing the JWS algorithm
|
|
"""
|
|
if strategy == cls.Strategy.RECOMMENDED:
|
|
algorithms = cls.filter_algorithms(key, cls.recommended)
|
|
elif strategy == cls.Strategy.SECURITY:
|
|
names = list(cls.algorithms.keys())
|
|
algorithms = cls.filter_algorithms(key, names)
|
|
# sort by security level
|
|
algorithms.sort(key=lambda alg: alg.algorithm_security, reverse=True)
|
|
else:
|
|
raise NotImplementedError(f"Unknown algorithm strategy '{strategy}'")
|
|
|
|
if algorithms:
|
|
return algorithms[0]
|
|
else:
|
|
return None
|
|
|
|
@classmethod
|
|
def guess_alg(cls, key: Any, strategy: Strategy) -> str | None: # pragma: no cover
|
|
warnings.warn("Please use guess_algorithm(key, strategy)", DeprecationWarning)
|
|
alg = cls.guess_algorithm(key, strategy)
|
|
if alg:
|
|
return alg.name
|
|
return None
|
|
|
|
@classmethod
|
|
def filter_algorithms(cls, key: Any, names: list[str] | None = None) -> list[JWSAlgModel]:
|
|
"""Filter JWS algorithms based on the given algorithm names.
|
|
|
|
:param key: a key instance or a KeySet
|
|
:param names: list of algorithm names
|
|
"""
|
|
if names is None:
|
|
names = list(cls.algorithms.keys())
|
|
rv: list[JWSAlgModel] = []
|
|
if isinstance(key, KeySet):
|
|
for k in key.keys:
|
|
for alg in cls.filter_algorithms(k, names):
|
|
if alg not in rv:
|
|
rv.append(alg)
|
|
return rv
|
|
|
|
for name in names:
|
|
alg = cls.algorithms[name]
|
|
try:
|
|
alg.check_key(key)
|
|
rv.append(alg)
|
|
except JoseError:
|
|
pass
|
|
return rv
|
|
|
|
|
|
#: default JWS registry
|
|
default_registry = JWSRegistry()
|
|
|
|
|
|
def construct_registry(algorithms: Collection[str] | None = None) -> JWSRegistry:
|
|
if algorithms:
|
|
registry = JWSRegistry(algorithms=algorithms)
|
|
else:
|
|
registry = default_registry
|
|
return registry
|