first commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user