first commit

This commit is contained in:
linkitapps
2026-09-14 23:22:10 +09:00
commit 9f9c8fb69b
4207 changed files with 1333187 additions and 0 deletions
@@ -0,0 +1,53 @@
from typing import Any
from .model import JWSAlgModel, CompactSignature
from ..errors import (
DecodeError,
MissingAlgorithmError,
)
from ..util import (
json_b64encode,
json_b64decode,
urlsafe_b64encode,
urlsafe_b64decode,
)
__all__ = [
"sign_compact",
"verify_compact",
"detach_compact_content",
"decode_header",
]
def sign_compact(obj: CompactSignature, alg: JWSAlgModel, key: Any) -> bytes:
header_segment = json_b64encode(obj.headers())
payload_segment = urlsafe_b64encode(obj.payload)
signing_input = header_segment + b"." + payload_segment
signature = urlsafe_b64encode(alg.sign(signing_input, key))
return signing_input + b"." + signature
def verify_compact(obj: CompactSignature, alg: JWSAlgModel, key: Any) -> bool:
signing_input = obj.segments["header"] + b"." + obj.segments["payload"]
try:
sig = urlsafe_b64decode(obj.segments["signature"])
except (TypeError, ValueError):
return False
return alg.verify(signing_input, sig, key)
def detach_compact_content(value: str) -> str:
# https://www.rfc-editor.org/rfc/rfc7515#appendix-F
parts = value.split(".")
parts[1] = ""
return ".".join(parts)
def decode_header(header_segment: bytes) -> dict[str, Any]:
try:
protected: dict[str, Any] = json_b64decode(header_segment)
if "alg" not in protected:
raise MissingAlgorithmError()
except (TypeError, ValueError):
raise DecodeError("Invalid header")
return protected
@@ -0,0 +1,166 @@
import copy
from typing import Any, Callable
from .model import (
HeaderMember,
GeneralJSONSignature,
FlattenedJSONSignature,
)
from .types import (
JSONSignatureDict,
GeneralJSONSerialization,
FlattenedJSONSerialization,
)
from .registry import JWSRegistry
from ..registry import reject_unprotected_crit_header
from ..util import (
to_bytes,
json_b64encode,
json_b64decode,
urlsafe_b64encode,
urlsafe_b64decode,
)
from ..errors import DecodeError
__all__ = [
"FindKey",
"sign_general_json",
"sign_flattened_json",
"sign_json_member",
"extract_general_json",
"verify_general_json",
"verify_flattened_json",
"detach_json_content",
]
FindKey = Callable[[HeaderMember], Any]
def sign_general_json(
members: list[HeaderMember],
payload: bytes,
registry: JWSRegistry,
find_key: FindKey,
) -> GeneralJSONSerialization:
payload_segment = urlsafe_b64encode(payload)
signatures: list[JSONSignatureDict] = [
sign_json_member(payload_segment, member, registry, find_key) for member in members
]
return {
"payload": payload_segment.decode("utf-8"),
"signatures": signatures,
}
def sign_flattened_json(
member: HeaderMember,
payload: bytes,
registry: JWSRegistry,
find_key: FindKey,
) -> FlattenedJSONSerialization:
payload_segment = urlsafe_b64encode(payload)
signature = sign_json_member(payload_segment, member, registry, find_key)
data: FlattenedJSONSerialization = {"payload": payload_segment.decode("utf-8"), **signature}
return data
def sign_json_member(
payload_segment: bytes, member: HeaderMember, registry: JWSRegistry, find_key: FindKey
) -> JSONSignatureDict:
reject_unprotected_crit_header(member.header)
headers = member.headers()
registry.check_header(headers)
alg = registry.get_alg(headers["alg"])
key = find_key(member)
alg.check_key(key)
if member.protected:
protected_segment = json_b64encode(member.protected)
else:
protected_segment = b""
signing_input = b".".join([protected_segment, payload_segment])
signature = urlsafe_b64encode(alg.sign(signing_input, key))
rv: JSONSignatureDict = {"signature": signature.decode("utf-8")}
if member.protected:
rv["protected"] = protected_segment.decode("utf-8")
if member.header:
rv["header"] = member.header
return rv
def extract_general_json(value: GeneralJSONSerialization, registry: JWSRegistry) -> GeneralJSONSignature:
payload_segment: bytes = value["payload"].encode("utf-8")
registry.validate_payload_size(payload_segment)
try:
payload = urlsafe_b64decode(payload_segment)
except (TypeError, ValueError):
raise DecodeError("Invalid payload")
signatures: list[JSONSignatureDict] = value["signatures"]
members = [__signature_to_member(sig, registry) for sig in signatures]
obj = GeneralJSONSignature(members, payload)
obj.signatures = signatures
obj.segments = {"payload": payload_segment}
return obj
def __signature_to_member(sig: JSONSignatureDict, registry: JWSRegistry) -> HeaderMember:
member = HeaderMember()
if "protected" in sig:
protected_segment = to_bytes(sig["protected"])
registry.validate_header_size(protected_segment)
member.protected = json_b64decode(protected_segment)
if "header" in sig:
member.header = sig["header"]
return member
def verify_general_json(obj: GeneralJSONSignature, registry: JWSRegistry, find_key: FindKey) -> bool:
payload_segment = obj.segments["payload"]
if not obj.signatures:
return False
for index, signature in enumerate(obj.signatures):
member = obj.members[index]
if not verify_signature(member, signature, payload_segment, registry, find_key):
return False
return True
def verify_flattened_json(obj: FlattenedJSONSignature, registry: JWSRegistry, find_key: FindKey) -> bool:
payload_segment = obj.segments["payload"]
assert obj.signature is not None
return verify_signature(obj.member, obj.signature, payload_segment, registry, find_key)
def verify_signature(
member: HeaderMember,
signature: JSONSignatureDict,
payload_segment: bytes,
registry: JWSRegistry,
find_key: FindKey,
) -> bool:
reject_unprotected_crit_header(member.header)
headers = member.headers()
registry.check_header(headers)
alg = registry.get_alg(headers["alg"])
key = find_key(member)
alg.check_key(key)
if "protected" in signature:
protected_segment = to_bytes(signature["protected"])
else:
protected_segment = b""
signature_segment = to_bytes(signature["signature"])
registry.validate_signature_size(signature_segment)
sig = urlsafe_b64decode(signature_segment)
signing_input = b".".join([protected_segment, payload_segment])
return alg.verify(signing_input, sig, key)
def detach_json_content(value: dict[str, Any]) -> dict[str, Any]:
# https://www.rfc-editor.org/rfc/rfc7515#appendix-F
rv = copy.deepcopy(value) # don't alter original value
if "payload" in rv:
del rv["payload"]
return rv
@@ -0,0 +1,141 @@
from typing import Any, ClassVar, Literal
from abc import ABCMeta, abstractmethod
from .types import SegmentsDict, JSONSignatureDict
from ..errors import InvalidKeyTypeError
from ..registry import Header
__all__ = [
"HeaderMember",
"CompactSignature",
"FlattenedJSONSignature",
"GeneralJSONSignature",
"JWSAlgModel",
]
class HeaderMember:
"""A header member of the JSON signature. It is combined with protected header,
and unprotected header.
"""
def __init__(self, protected: Header | None = None, header: Header | None = None):
#: protected header
self.protected = protected
#: unprotected header
self.header = header
def headers(self) -> Header:
rv: Header = {}
if self.header:
rv.update(self.header)
# protected header is preferred
if self.protected:
rv.update(self.protected)
return rv
def set_kid(self, kid: str) -> None:
if self.header is None:
self.header = {}
self.header["kid"] = kid
class CompactSignature:
"""JSON Web Signature object for compact mode. This object is used to
represent the JWS instance.
"""
def __init__(self, protected: Header, payload: bytes):
#: protected header
self.protected = protected
#: payload content in bytes
self.payload = payload
self.segments: SegmentsDict = {}
def headers(self) -> Header:
"""Returns protected header values in dict."""
return self.protected
def set_kid(self, kid: str) -> None:
self.protected["kid"] = kid
class FlattenedJSONSignature:
"""JSON Signature object that represents a flattened JSON serialization."""
#: mark it as flattened
flattened: ClassVar[bool] = True
def __init__(self, member: HeaderMember, payload: bytes):
#: the only header member
self.member: HeaderMember = member
#: payload content in bytes
self.payload: bytes = payload
self.signature: JSONSignatureDict | None = None
self.segments: SegmentsDict = {}
@property
def members(self) -> list[HeaderMember]:
"""A list of header members. For flattened JSON serialization, there will
be only one header member."""
return [self.member]
def headers(self) -> Header:
"""Header values in dict."""
return self.member.headers()
class GeneralJSONSignature:
"""JSON Signature object that represents a general JSON serialization."""
#: mark it as not flattened (general)
flattened: ClassVar[bool] = False
def __init__(self, members: list[HeaderMember], payload: bytes):
#: a list of header members
self.members: list[HeaderMember] = members
#: payload content in bytes
self.payload: bytes = payload
self.signatures: list[JSONSignatureDict] = []
self.segments: SegmentsDict = {}
class JWSAlgModel(metaclass=ABCMeta):
"""Interface for JWS algorithm. JWA specification (RFC7518) SHOULD
implement the algorithms for JWS with this base implementation.
"""
name: str
description: str
recommended: bool = False
security_warning: str | None = None
key_type = "oct"
algorithm_type: Literal["JWS"] = "JWS"
algorithm_location = "sig"
algorithm_security = 0
def check_key(self, key: Any) -> None:
key.check_use("sig")
if key.key_type != self.key_type:
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires '{self.key_type}' key")
key.check_alg(self.name)
@abstractmethod
def sign(self, msg: bytes, key: Any) -> bytes:
"""Sign the text msg with a private/sign key.
:param msg: message bytes to be signed
:param key: private key to sign the message
:return: bytes
"""
@abstractmethod
def verify(self, msg: bytes, sig: bytes, key: Any) -> bool:
"""Verify the signature of text msg with a public/verify key.
:param msg: message bytes to be signed
:param sig: result signature to be compared
:param key: public key to verify the signature
:return: boolean
"""
@@ -0,0 +1,181 @@
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
@@ -0,0 +1,41 @@
from typing import TypedDict, final
from ..registry import Header
__all__ = [
"SegmentsDict",
"HeaderDict",
"JSONSignatureDict",
"GeneralJSONSerialization",
"FlattenedJSONSerialization",
]
class SegmentsDict(TypedDict, total=False):
header: bytes
payload: bytes
signature: bytes
class HeaderDict(TypedDict, total=False):
protected: Header
header: Header
class JSONSignatureDict(TypedDict, total=False):
protected: str
header: Header
signature: str
@final
class GeneralJSONSerialization(TypedDict):
payload: str
signatures: list[JSONSignatureDict]
@final
class FlattenedJSONSerialization(TypedDict, total=False):
payload: str
protected: str
header: Header
signature: str