first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
__version__ = "1.7.5"
|
||||
__homepage__ = "https://jose.authlib.org/en/"
|
||||
__author__ = "Hsiaoming Yang <me@lepture.com>"
|
||||
__license__ = "BSD-3-Clause"
|
||||
@@ -0,0 +1,213 @@
|
||||
import typing as t
|
||||
import random
|
||||
from ._rfc7517.types import AnyKey, KeyParameters, DictKey
|
||||
from ._rfc7518.oct_key import OctKey
|
||||
from ._rfc7518.rsa_key import RSAKey
|
||||
from ._rfc7518.ec_key import ECKey
|
||||
from ._rfc8037.okp_key import OKPKey
|
||||
from .errors import (
|
||||
MissingKeyError,
|
||||
InvalidKeyIdError,
|
||||
InvalidKeyTypeError,
|
||||
MissingKeyTypeError,
|
||||
)
|
||||
from .util import to_bytes
|
||||
|
||||
__all__ = [
|
||||
"OctKey",
|
||||
"RSAKey",
|
||||
"ECKey",
|
||||
"OKPKey",
|
||||
"Key",
|
||||
"KeySet",
|
||||
"JWKRegistry",
|
||||
"KeySetSerialization",
|
||||
]
|
||||
|
||||
Key = t.Union[OctKey, RSAKey, ECKey, OKPKey]
|
||||
|
||||
|
||||
class JWKRegistry:
|
||||
"""A registry for JWK to record ``joserfc`` supported key types.
|
||||
Normally, you would use explicit key types like ``OctKey``, ``RSAKey``;
|
||||
This registry provides a way to dynamically import and generate keys.
|
||||
For instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from joserfc.jwk import JWKRegistry
|
||||
|
||||
# instead of choosing which key type to use yourself,
|
||||
# JWKRegistry can import it automatically
|
||||
data = {"kty": "oct", "k": "..."}
|
||||
key = JWKRegistry.import_key(data)
|
||||
"""
|
||||
|
||||
key_types: dict[str, type[Key]] = {
|
||||
OctKey.key_type: OctKey,
|
||||
RSAKey.key_type: RSAKey,
|
||||
ECKey.key_type: ECKey,
|
||||
OKPKey.key_type: OKPKey,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def import_key(cls, data: AnyKey, key_type: str | None = None, parameters: KeyParameters | None = None) -> Key:
|
||||
"""A class method for importing a key from bytes, string, and dict.
|
||||
When ``value`` is a dict, this method can tell the key type automatically,
|
||||
otherwise, developers SHOULD pass the ``key_type`` themselves.
|
||||
|
||||
:param data: the key data in bytes, string, or dict.
|
||||
:param key_type: an optional key type in string.
|
||||
:param parameters: extra key parameters
|
||||
:return: OctKey, RSAKey, ECKey, or OKPKey
|
||||
"""
|
||||
if isinstance(data, dict) and key_type is None:
|
||||
if "kty" in data:
|
||||
key_type = t.cast(str, data["kty"])
|
||||
else:
|
||||
raise MissingKeyTypeError("Missing key type")
|
||||
|
||||
if key_type not in cls.key_types:
|
||||
raise InvalidKeyTypeError(f"Invalid key type: '{key_type}'")
|
||||
|
||||
if isinstance(data, str):
|
||||
data = to_bytes(data)
|
||||
|
||||
key_cls = cls.key_types[key_type]
|
||||
return key_cls.import_key(data, parameters)
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls,
|
||||
key_type: str,
|
||||
crv_or_size: str | int | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> Key:
|
||||
"""A class method for generating key according to the given key type.
|
||||
When ``key_type`` is "oct" and "RSA", the second parameter SHOULD be
|
||||
a key size in bits. When ``key_type`` is "EC" and "OKP", the second
|
||||
parameter SHOULD be a "crv" string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
JWKRegistry.generate_key("RSA", 2048)
|
||||
JWKRegistry.generate_key("EC", "P-256")
|
||||
"""
|
||||
if key_type not in cls.key_types:
|
||||
raise InvalidKeyTypeError(f"Invalid key type: '{key_type}'")
|
||||
|
||||
key_cls = cls.key_types[key_type]
|
||||
return key_cls.generate_key(crv_or_size, parameters, private, auto_kid) # type: ignore[arg-type]
|
||||
|
||||
|
||||
KeySetSerialization = t.TypedDict("KeySetSerialization", {"keys": list[DictKey]})
|
||||
|
||||
|
||||
class KeySet:
|
||||
#: keys in the key set
|
||||
keys: list[Key]
|
||||
|
||||
registry_cls: type[JWKRegistry] = JWKRegistry
|
||||
algorithm_keys: t.ClassVar[dict[str, list[str]]] = {}
|
||||
|
||||
def __init__(self, keys: list[Key]):
|
||||
for key in keys:
|
||||
key.ensure_kid()
|
||||
self.keys = keys
|
||||
|
||||
def __iter__(self) -> t.Iterator[Key]:
|
||||
return iter(self.keys)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.keys)
|
||||
|
||||
def __eq__(self, other: t.Any) -> bool:
|
||||
assert isinstance(other, KeySet)
|
||||
return self.keys == other.keys
|
||||
|
||||
def as_dict(self, private: bool = False, **params: t.Any) -> KeySetSerialization:
|
||||
keys: list[DictKey] = []
|
||||
|
||||
for key in self.keys:
|
||||
# trigger key to generate kid via thumbprint
|
||||
key.ensure_kid()
|
||||
keys.append(key.as_dict(private=private, **params))
|
||||
return {"keys": keys}
|
||||
|
||||
def get_by_kid(self, kid: str | None = None, parameters: KeyParameters | None = None) -> Key:
|
||||
if kid is None and len(self.keys) == 1:
|
||||
return self.keys[0]
|
||||
|
||||
keys = [key for key in self.keys if key.kid == kid]
|
||||
if parameters:
|
||||
keys = list(_filter_keys_by_parameters(keys, parameters))
|
||||
|
||||
if keys:
|
||||
return keys[0]
|
||||
raise InvalidKeyIdError(f"No key for kid: '{kid}'")
|
||||
|
||||
def pick_random_key(self, algorithm: str | None = None, parameters: KeyParameters | None = None) -> Key | None:
|
||||
key_types = self.algorithm_keys.get(algorithm) if algorithm else None
|
||||
if key_types:
|
||||
keys = [k for k in self.keys if k.key_type in key_types]
|
||||
else:
|
||||
keys = self.keys
|
||||
|
||||
if parameters:
|
||||
keys = list(_filter_keys_by_parameters(keys, parameters))
|
||||
|
||||
if keys:
|
||||
return random.choice(keys)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def import_key_set(cls, value: KeySetSerialization, parameters: KeyParameters | None = None) -> "KeySet":
|
||||
keys: list[Key] = []
|
||||
|
||||
for data in value["keys"]:
|
||||
# RFC 7517, Section 5: ignore a key whose "kty" is not understood
|
||||
# rather than failing the whole set (for example a post-quantum key
|
||||
# published alongside classical ones).
|
||||
try:
|
||||
keys.append(cls.registry_cls.import_key(data, parameters=parameters))
|
||||
except InvalidKeyTypeError:
|
||||
continue
|
||||
|
||||
if not keys:
|
||||
raise MissingKeyError("No keys to import")
|
||||
|
||||
return cls(keys)
|
||||
|
||||
@classmethod
|
||||
def generate_key_set(
|
||||
cls,
|
||||
key_type: str,
|
||||
crv_or_size: str | int,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
count: int = 4,
|
||||
) -> "KeySet":
|
||||
keys: list[Key] = []
|
||||
for _ in range(count):
|
||||
key = cls.registry_cls.generate_key(key_type, crv_or_size, parameters, private)
|
||||
keys.append(key)
|
||||
|
||||
return cls(keys)
|
||||
|
||||
|
||||
def _filter_keys_by_parameters(keys: list[Key], parameters: KeyParameters) -> t.Iterator[Key]:
|
||||
_use = parameters.get("use")
|
||||
_alg = parameters.get("alg")
|
||||
|
||||
for key in keys:
|
||||
designed_use = key.get("use")
|
||||
if designed_use and _use and designed_use != _use:
|
||||
continue
|
||||
|
||||
designed_alg = key.get("alg")
|
||||
if designed_alg and _alg and designed_alg != _alg:
|
||||
continue
|
||||
|
||||
yield key
|
||||
@@ -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
|
||||
@@ -0,0 +1,75 @@
|
||||
from .models import CompactEncryption, Recipient
|
||||
from .registry import JWERegistry
|
||||
from .._keys import Key
|
||||
from ..errors import (
|
||||
MissingAlgorithmError,
|
||||
MissingEncryptionError,
|
||||
DecodeError,
|
||||
)
|
||||
from ..util import (
|
||||
json_b64decode,
|
||||
urlsafe_b64encode,
|
||||
urlsafe_b64decode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"represent_compact",
|
||||
"extract_compact",
|
||||
]
|
||||
|
||||
|
||||
def represent_compact(obj: CompactEncryption) -> bytes:
|
||||
assert obj.recipient is not None
|
||||
encrypted_key = obj.recipient.encrypted_key
|
||||
assert encrypted_key is not None
|
||||
return b".".join(
|
||||
[
|
||||
obj.base64_segments["aad"],
|
||||
urlsafe_b64encode(encrypted_key),
|
||||
obj.base64_segments["iv"],
|
||||
obj.base64_segments["ciphertext"],
|
||||
obj.base64_segments["tag"],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def extract_compact(value: bytes, registry: JWERegistry) -> CompactEncryption:
|
||||
parts = value.split(b".")
|
||||
if len(parts) != 5:
|
||||
raise ValueError("Invalid JSON Web Encryption")
|
||||
|
||||
header_segment, ek_segment, iv_segment, ciphertext_segment, tag_segment = parts
|
||||
registry.validate_protected_header_size(header_segment)
|
||||
registry.validate_encrypted_key_size(ek_segment)
|
||||
registry.validate_initialization_vector_size(iv_segment)
|
||||
registry.validate_ciphertext_size(ciphertext_segment)
|
||||
registry.validate_auth_tag_size(tag_segment)
|
||||
try:
|
||||
protected = json_b64decode(header_segment)
|
||||
if "alg" not in protected:
|
||||
raise MissingAlgorithmError()
|
||||
if "enc" not in protected:
|
||||
raise MissingEncryptionError()
|
||||
except (TypeError, ValueError):
|
||||
raise DecodeError("Invalid header")
|
||||
|
||||
obj = CompactEncryption(protected)
|
||||
obj.base64_segments.update(
|
||||
{
|
||||
"aad": header_segment,
|
||||
"iv": iv_segment,
|
||||
"ciphertext": ciphertext_segment,
|
||||
"tag": tag_segment,
|
||||
}
|
||||
)
|
||||
obj.bytes_segments.update(
|
||||
{
|
||||
"iv": urlsafe_b64decode(iv_segment),
|
||||
"ciphertext": urlsafe_b64decode(ciphertext_segment),
|
||||
"tag": urlsafe_b64decode(tag_segment),
|
||||
}
|
||||
)
|
||||
recipient: Recipient[Key] = Recipient(obj)
|
||||
recipient.encrypted_key = urlsafe_b64decode(ek_segment)
|
||||
obj.recipient = recipient
|
||||
return obj
|
||||
@@ -0,0 +1,142 @@
|
||||
from typing import Any
|
||||
from .models import (
|
||||
BaseJSONEncryption,
|
||||
GeneralJSONEncryption,
|
||||
FlattenedJSONEncryption,
|
||||
Recipient,
|
||||
)
|
||||
from .registry import JWERegistry
|
||||
from .types import (
|
||||
JSONRecipientDict,
|
||||
GeneralJSONSerialization,
|
||||
FlattenedJSONSerialization,
|
||||
)
|
||||
from ..util import (
|
||||
to_bytes,
|
||||
to_str,
|
||||
json_b64encode,
|
||||
json_b64decode,
|
||||
urlsafe_b64encode,
|
||||
urlsafe_b64decode,
|
||||
)
|
||||
from .._keys import Key
|
||||
|
||||
|
||||
__all__ = [
|
||||
"represent_general_json",
|
||||
"represent_flattened_json",
|
||||
"extract_general_json",
|
||||
"extract_flattened_json",
|
||||
]
|
||||
|
||||
|
||||
def represent_general_json(obj: GeneralJSONEncryption) -> GeneralJSONSerialization:
|
||||
data: GeneralJSONSerialization = __represent_json_serialization(obj)
|
||||
recipients = []
|
||||
for recipient in obj.recipients:
|
||||
item: JSONRecipientDict = {}
|
||||
assert recipient.header is not None
|
||||
assert recipient.encrypted_key is not None
|
||||
item["header"] = recipient.header
|
||||
item["encrypted_key"] = to_str(urlsafe_b64encode(recipient.encrypted_key))
|
||||
recipients.append(item)
|
||||
data["recipients"] = recipients
|
||||
return data
|
||||
|
||||
|
||||
def represent_flattened_json(obj: FlattenedJSONEncryption) -> FlattenedJSONSerialization:
|
||||
data: FlattenedJSONSerialization = __represent_json_serialization(obj)
|
||||
recipient = obj.recipients[0]
|
||||
assert recipient is not None
|
||||
assert recipient.header is not None
|
||||
assert recipient.encrypted_key is not None
|
||||
data["header"] = recipient.header
|
||||
data["encrypted_key"] = to_str(urlsafe_b64encode(recipient.encrypted_key))
|
||||
return data
|
||||
|
||||
|
||||
def __represent_json_serialization(obj: BaseJSONEncryption) -> Any:
|
||||
data: dict[str, Any] = {
|
||||
"protected": to_str(json_b64encode(obj.protected)),
|
||||
"iv": to_str(obj.base64_segments["iv"]),
|
||||
"ciphertext": to_str(obj.base64_segments["ciphertext"]),
|
||||
"tag": to_str(obj.base64_segments["tag"]),
|
||||
}
|
||||
if obj.aad:
|
||||
data["aad"] = to_str(urlsafe_b64encode(obj.aad))
|
||||
|
||||
if obj.unprotected:
|
||||
data["unprotected"] = obj.unprotected
|
||||
return data
|
||||
|
||||
|
||||
def extract_general_json(data: GeneralJSONSerialization, registry: JWERegistry) -> GeneralJSONEncryption:
|
||||
protected_segment = to_bytes(data["protected"])
|
||||
registry.validate_protected_header_size(protected_segment)
|
||||
protected = json_b64decode(protected_segment)
|
||||
|
||||
unprotected = data.get("unprotected")
|
||||
recipients = data["recipients"]
|
||||
registry.validate_recipient_count(recipients)
|
||||
base64_segments, bytes_segments, aad = __extract_segments(data, registry)
|
||||
|
||||
obj = GeneralJSONEncryption(protected, None, unprotected, aad)
|
||||
obj.base64_segments = base64_segments
|
||||
obj.bytes_segments = bytes_segments
|
||||
for item in recipients:
|
||||
recipient = __extract_recipient(obj, item, registry)
|
||||
obj.recipients.append(recipient)
|
||||
return obj
|
||||
|
||||
|
||||
def extract_flattened_json(data: FlattenedJSONSerialization, registry: JWERegistry) -> FlattenedJSONEncryption:
|
||||
protected_segment = to_bytes(data["protected"])
|
||||
registry.validate_protected_header_size(protected_segment)
|
||||
protected = json_b64decode(protected_segment)
|
||||
unprotected = data.get("unprotected")
|
||||
base64_segments, bytes_segments, aad = __extract_segments(data, registry)
|
||||
obj = FlattenedJSONEncryption(protected, None, unprotected, aad)
|
||||
obj.base64_segments = base64_segments
|
||||
obj.bytes_segments = bytes_segments
|
||||
recipient = __extract_recipient(obj, data, registry)
|
||||
obj.recipients.append(recipient)
|
||||
return obj
|
||||
|
||||
|
||||
def __extract_segments(
|
||||
data: GeneralJSONSerialization | FlattenedJSONSerialization,
|
||||
registry: JWERegistry,
|
||||
) -> tuple[dict[str, bytes], dict[str, bytes], bytes | None]:
|
||||
base64_segments: dict[str, bytes] = {
|
||||
"iv": to_bytes(data["iv"]),
|
||||
"ciphertext": to_bytes(data["ciphertext"]),
|
||||
"tag": to_bytes(data["tag"]),
|
||||
}
|
||||
registry.validate_initialization_vector_size(base64_segments["iv"])
|
||||
registry.validate_ciphertext_size(base64_segments["ciphertext"])
|
||||
registry.validate_auth_tag_size(base64_segments["tag"])
|
||||
bytes_segments: dict[str, bytes] = {
|
||||
"iv": urlsafe_b64decode(base64_segments["iv"]),
|
||||
"ciphertext": urlsafe_b64decode(base64_segments["ciphertext"]),
|
||||
"tag": urlsafe_b64decode(base64_segments["tag"]),
|
||||
}
|
||||
if "aad" in data:
|
||||
base64_segments["aad"] = to_bytes(data["aad"])
|
||||
aad = urlsafe_b64decode(base64_segments["aad"])
|
||||
bytes_segments["aad"] = aad
|
||||
else:
|
||||
aad = None
|
||||
return base64_segments, bytes_segments, aad
|
||||
|
||||
|
||||
def __extract_recipient(
|
||||
obj: FlattenedJSONEncryption | GeneralJSONEncryption,
|
||||
data: FlattenedJSONSerialization | JSONRecipientDict,
|
||||
registry: JWERegistry,
|
||||
) -> Recipient[Key]:
|
||||
recipient: Recipient[Key] = Recipient(obj, data.get("header"))
|
||||
if "encrypted_key" in data:
|
||||
ek_segment = to_bytes(data["encrypted_key"])
|
||||
registry.validate_encrypted_key_size(ek_segment)
|
||||
recipient.encrypted_key = urlsafe_b64decode(ek_segment)
|
||||
return recipient
|
||||
@@ -0,0 +1,266 @@
|
||||
import secrets
|
||||
from typing import Any, Union
|
||||
|
||||
from .models import (
|
||||
CompactEncryption,
|
||||
BaseJSONEncryption,
|
||||
GeneralJSONEncryption,
|
||||
FlattenedJSONEncryption,
|
||||
Recipient,
|
||||
JWEAlgModel,
|
||||
JWEEncModel,
|
||||
JWEKeyAgreement,
|
||||
JWEDirectEncryption,
|
||||
JWEKeyEncryption,
|
||||
JWEKeyWrapping,
|
||||
)
|
||||
from .registry import JWERegistry
|
||||
from ..errors import (
|
||||
JoseError,
|
||||
DecodeError,
|
||||
InvalidEncryptedKeyError,
|
||||
InvalidExchangeKeyError,
|
||||
ConflictAlgorithmError,
|
||||
)
|
||||
from ..util import (
|
||||
json_b64encode,
|
||||
urlsafe_b64encode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"EncryptionData",
|
||||
"perform_encrypt",
|
||||
"perform_decrypt",
|
||||
]
|
||||
|
||||
EncryptionData = Union[CompactEncryption, GeneralJSONEncryption, FlattenedJSONEncryption]
|
||||
|
||||
|
||||
def perform_encrypt(obj: EncryptionData, registry: JWERegistry) -> None:
|
||||
enc = registry.get_enc(obj.protected["enc"])
|
||||
cek, delayed_tasks = pre_encrypt_recipients(enc, obj.recipients, registry)
|
||||
|
||||
# Step 9, Generate a random JWE Initialization Vector of the correct size
|
||||
# for the content encryption algorithm (if required for the algorithm);
|
||||
# otherwise, let the JWE Initialization Vector be the empty octet sequence.
|
||||
iv = enc.generate_iv()
|
||||
|
||||
# Step 10, Compute the encoded Initialization Vector value
|
||||
# BASE64URL(JWE Initialization Vector).
|
||||
obj.base64_segments["iv"] = urlsafe_b64encode(iv)
|
||||
|
||||
# Step 11, If a "zip" parameter was included, compress the plaintext using
|
||||
# the specified compression algorithm and let M be the octet sequence
|
||||
# representing the compressed plaintext; otherwise, let M be the octet
|
||||
# sequence representing the plaintext.
|
||||
assert obj.plaintext is not None
|
||||
|
||||
plaintext: bytes
|
||||
if "zip" in obj.protected:
|
||||
zip_ = registry.get_zip(obj.protected["zip"])
|
||||
plaintext = zip_.compress(obj.plaintext)
|
||||
else:
|
||||
plaintext = obj.plaintext
|
||||
|
||||
# Step 13, Compute the Encoded Protected Header value BASE64URL(UTF8(JWE Protected Header)).
|
||||
aad = json_b64encode(obj.protected)
|
||||
|
||||
# Step 14, Let the Additional Authenticated Data encryption parameter be
|
||||
# ASCII(Encoded Protected Header). However, if a JWE AAD value is
|
||||
# present (which can only be the case when using the JWE JSON Serialization),
|
||||
# instead let the Additional Authenticated Data encryption parameter be
|
||||
# ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)).
|
||||
if isinstance(obj, BaseJSONEncryption) and obj.aad:
|
||||
aad = aad + b"." + urlsafe_b64encode(obj.aad)
|
||||
obj.base64_segments["aad"] = aad
|
||||
|
||||
# encrypting plaintext
|
||||
ciphertext, tag = enc.encrypt(plaintext, cek, iv, aad)
|
||||
|
||||
# delay encrypting every recipient
|
||||
post_encrypt_recipients(enc, delayed_tasks, cek, tag)
|
||||
|
||||
obj.base64_segments["ciphertext"] = urlsafe_b64encode(ciphertext)
|
||||
obj.base64_segments["tag"] = urlsafe_b64encode(tag)
|
||||
|
||||
|
||||
def perform_decrypt(obj: EncryptionData, registry: JWERegistry) -> None:
|
||||
try:
|
||||
_perform_decrypt(obj, registry)
|
||||
except InvalidExchangeKeyError as error:
|
||||
raise DecodeError(error.description)
|
||||
|
||||
|
||||
def _perform_decrypt(obj: EncryptionData, registry: JWERegistry) -> None:
|
||||
enc = registry.get_enc(obj.protected["enc"])
|
||||
|
||||
iv = obj.bytes_segments["iv"]
|
||||
enc.check_iv(iv)
|
||||
|
||||
tag = obj.bytes_segments["tag"]
|
||||
ciphertext = obj.bytes_segments["ciphertext"]
|
||||
|
||||
cek_set = set()
|
||||
for recipient in obj.recipients:
|
||||
try:
|
||||
headers = recipient.headers()
|
||||
registry.check_header(headers, True)
|
||||
# Step 6, Determine the Key Management Mode employed by the algorithm
|
||||
# specified by the "alg" (algorithm) Header Parameter.
|
||||
alg = registry.get_alg(headers["alg"])
|
||||
cek = decrypt_recipient(alg, enc, recipient, tag)
|
||||
cek_set.add(cek)
|
||||
except (AssertionError, JoseError) as error:
|
||||
if registry.verify_all_recipients:
|
||||
raise error
|
||||
else:
|
||||
if not registry.verify_all_recipients:
|
||||
break
|
||||
|
||||
if not cek_set:
|
||||
raise DecodeError("Invalid recipients")
|
||||
|
||||
if len(cek_set) > 1: # pragma: no cover
|
||||
raise DecodeError("Multiple 'cek' found")
|
||||
|
||||
cek = cek_set.pop()
|
||||
if len(cek) * 8 != enc.cek_size: # pragma: no cover
|
||||
cek = secrets.token_bytes(enc.cek_size // 8)
|
||||
|
||||
if isinstance(obj, BaseJSONEncryption):
|
||||
aad = json_b64encode(obj.protected)
|
||||
if obj.aad:
|
||||
aad = aad + b"." + obj.base64_segments["aad"]
|
||||
else:
|
||||
aad = obj.base64_segments["aad"]
|
||||
|
||||
msg = enc.decrypt(ciphertext, tag, cek, iv, aad)
|
||||
if "zip" in obj.protected:
|
||||
zip_ = registry.get_zip(obj.protected["zip"])
|
||||
obj.plaintext = zip_.decompress(msg)
|
||||
else:
|
||||
obj.plaintext = msg
|
||||
|
||||
|
||||
def pre_encrypt_recipients(
|
||||
enc: JWEEncModel, recipients: list[Recipient[Any]], registry: JWERegistry
|
||||
) -> tuple[bytes, list[tuple[JWEKeyAgreement, Recipient[Any]]]]:
|
||||
cek: bytes = b""
|
||||
delayed_tasks: list[tuple[JWEKeyAgreement, Recipient[Any]]] = []
|
||||
for recipient in recipients:
|
||||
alg = __prepare_recipient_algorithm(recipient, registry)
|
||||
|
||||
if alg.direct_mode:
|
||||
if len(recipients) > 1:
|
||||
raise ConflictAlgorithmError(f"Algorithm {alg.name} SHOULD have 1 recipient only")
|
||||
cek = __pre_encrypt_direct_mode(alg, enc, recipient)
|
||||
else:
|
||||
if not cek:
|
||||
# 2. When Key Wrapping, Key Encryption, or Key Agreement with Key
|
||||
# Wrapping are employed, generate a random CEK value. See RFC
|
||||
# 4086 [RFC4086] for considerations on generating random values.
|
||||
# The CEK MUST have a length equal to that required for the
|
||||
# content encryption algorithm.
|
||||
cek = enc.generate_cek()
|
||||
|
||||
if isinstance(alg, JWEKeyAgreement):
|
||||
delayed_tasks.append((alg, recipient))
|
||||
else:
|
||||
# 4. When Key Wrapping, or Key Encryption are employed, encrypt the CEK
|
||||
# to the recipient and let the result be the JWE Encrypted Key.
|
||||
assert isinstance(alg, (JWEKeyWrapping, JWEKeyEncryption))
|
||||
recipient.encrypted_key = alg.encrypt_cek(cek, recipient)
|
||||
return cek, delayed_tasks
|
||||
|
||||
|
||||
def __prepare_recipient_algorithm(recipient: Recipient[Any], registry: JWERegistry) -> JWEAlgModel:
|
||||
headers = recipient.headers()
|
||||
registry.check_header(headers)
|
||||
# 1. Determine the Key Management Mode employed by the algorithm used
|
||||
# to determine the Content Encryption Key value. (This is the
|
||||
# algorithm recorded in the "alg" (algorithm) Header Parameter of
|
||||
# the resulting JWE.)
|
||||
alg = registry.get_alg(headers["alg"])
|
||||
|
||||
if isinstance(alg, JWEKeyAgreement):
|
||||
alg.prepare_ephemeral_key(recipient)
|
||||
return alg
|
||||
|
||||
|
||||
def __pre_encrypt_direct_mode(alg: JWEAlgModel, enc: JWEEncModel, recipient: Recipient[Any]) -> bytes:
|
||||
cek: bytes
|
||||
if isinstance(alg, JWEKeyAgreement):
|
||||
# 3. When Direct Key Agreement is employed,
|
||||
# let the CEK be the agreed upon key.
|
||||
cek = alg.encrypt_agreed_upon_key(enc, recipient)
|
||||
if len(cek) * 8 != enc.cek_size: # pragma: no cover
|
||||
cek = secrets.token_bytes(enc.cek_size // 8)
|
||||
else:
|
||||
# 6. When Direct Encryption is employed, let the CEK be the shared
|
||||
# symmetric key.
|
||||
assert isinstance(alg, JWEDirectEncryption)
|
||||
cek = alg.compute_cek(enc.cek_size, recipient)
|
||||
|
||||
# 5. When Direct Key Agreement or Direct Encryption are employed, let
|
||||
# the JWE Encrypted Key be the empty octet sequence.
|
||||
recipient.encrypted_key = b""
|
||||
return cek
|
||||
|
||||
|
||||
def post_encrypt_recipients(
|
||||
enc: JWEEncModel, tasks: list[tuple[JWEKeyAgreement, Recipient[Any]]], cek: bytes, tag: bytes
|
||||
) -> None:
|
||||
for alg, recipient in tasks:
|
||||
if alg.tag_aware:
|
||||
agreed_upon_key = alg.encrypt_agreed_upon_key_with_tag(enc, recipient, tag)
|
||||
else:
|
||||
agreed_upon_key = alg.encrypt_agreed_upon_key(enc, recipient)
|
||||
# 4. When Key Agreement with Key Wrapping is employed, encrypt the CEK
|
||||
# to the recipient and let the result be the JWE Encrypted Key.
|
||||
recipient.encrypted_key = alg.wrap_cek_with_auk(cek, agreed_upon_key)
|
||||
|
||||
|
||||
def decrypt_recipient(alg: JWEAlgModel, enc: JWEEncModel, recipient: Recipient[Any], tag: bytes) -> bytes:
|
||||
cek: bytes
|
||||
if alg.direct_mode:
|
||||
# 10. When Direct Key Agreement or Direct Encryption are employed,
|
||||
# verify that the JWE Encrypted Key value is an empty octet
|
||||
# sequence.
|
||||
if recipient.encrypted_key: # pragma: no cover
|
||||
raise InvalidEncryptedKeyError()
|
||||
|
||||
if isinstance(alg, JWEKeyAgreement):
|
||||
# 8. When Direct Key Agreement is employed, let the CEK be the agreed upon key.
|
||||
cek = alg.decrypt_agreed_upon_key(enc, recipient)
|
||||
else:
|
||||
# 11. When Direct Encryption is employed, let the CEK be the shared
|
||||
# symmetric key.
|
||||
assert isinstance(alg, JWEDirectEncryption)
|
||||
cek = alg.compute_cek(enc.cek_size, recipient)
|
||||
elif isinstance(alg, JWEKeyAgreement):
|
||||
agreed_upon_key: bytes
|
||||
if alg.tag_aware:
|
||||
agreed_upon_key = alg.decrypt_agreed_upon_key_with_tag(enc, recipient, tag)
|
||||
else:
|
||||
agreed_upon_key = alg.decrypt_agreed_upon_key(enc, recipient)
|
||||
|
||||
# 8. When Key Agreement with Key Wrapping is employed, the agreed upon key
|
||||
# will be used to decrypt the JWE Encrypted Key.
|
||||
if recipient.encrypted_key is None: # pragma: no cover
|
||||
raise DecodeError("Invalid encrypted key")
|
||||
cek = alg.unwrap_cek_with_auk(recipient.encrypted_key, agreed_upon_key)
|
||||
else:
|
||||
# 9. When Key Wrapping, Key Encryption, or Key Agreement with Key
|
||||
# Wrapping are employed, decrypt the JWE Encrypted Key to produce
|
||||
# the CEK. The CEK MUST have a length equal to that required for
|
||||
# the content encryption algorithm. Note that when there are
|
||||
# multiple recipients, each recipient will only be able to decrypt
|
||||
# JWE Encrypted Key values that were encrypted to a key in that
|
||||
# recipient's possession. It is therefore normal to only be able
|
||||
# to decrypt one of the per-recipient JWE Encrypted Key values to
|
||||
# obtain the CEK value.
|
||||
assert isinstance(alg, (JWEKeyWrapping, JWEKeyEncryption))
|
||||
if recipient.encrypted_key is None:
|
||||
raise DecodeError("Invalid encrypted key")
|
||||
cek = alg.decrypt_cek(recipient)
|
||||
return cek
|
||||
@@ -0,0 +1,345 @@
|
||||
import typing as t
|
||||
import secrets
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from ..registry import Header, HeaderRegistryDict
|
||||
from ..errors import InvalidKeyTypeError, InvalidKeyLengthError
|
||||
from .._keys import Key, ECKey, OctKey
|
||||
|
||||
__all__ = [
|
||||
"Recipient",
|
||||
"CompactEncryption",
|
||||
"BaseJSONEncryption",
|
||||
"GeneralJSONEncryption",
|
||||
"FlattenedJSONEncryption",
|
||||
"JWEEncModel",
|
||||
"JWEZipModel",
|
||||
"KeyManagement",
|
||||
"JWEDirectEncryption",
|
||||
"JWEKeyEncryption",
|
||||
"JWEKeyWrapping",
|
||||
"JWEKeyAgreement",
|
||||
"JWEAlgModel",
|
||||
]
|
||||
|
||||
KeyType = t.TypeVar("KeyType")
|
||||
|
||||
|
||||
class Recipient(t.Generic[KeyType]):
|
||||
def __init__(
|
||||
self,
|
||||
parent: t.Union["CompactEncryption", "GeneralJSONEncryption", "FlattenedJSONEncryption"],
|
||||
header: Header | None = None,
|
||||
recipient_key: KeyType | None = None,
|
||||
):
|
||||
self.__parent = parent
|
||||
self.header = header
|
||||
self.recipient_key = recipient_key
|
||||
self.sender_key: KeyType | None = None
|
||||
self.encrypted_key: bytes | None = None
|
||||
self.ephemeral_key: KeyType | None = None
|
||||
|
||||
def headers(self) -> Header:
|
||||
rv: Header = {}
|
||||
if isinstance(self.__parent, BaseJSONEncryption) and self.__parent.unprotected:
|
||||
rv.update(self.__parent.unprotected)
|
||||
if self.header:
|
||||
rv.update(self.header)
|
||||
|
||||
rv.update(self.__parent.protected)
|
||||
return rv
|
||||
|
||||
def add_header(self, k: str, v: t.Any) -> None:
|
||||
if isinstance(self.__parent, CompactEncryption):
|
||||
self.__parent.protected.update({k: v})
|
||||
elif self.header:
|
||||
self.header.update({k: v})
|
||||
else:
|
||||
self.header = {k: v}
|
||||
|
||||
def set_kid(self, kid: str) -> None:
|
||||
self.add_header("kid", kid)
|
||||
|
||||
|
||||
class CompactEncryption:
|
||||
"""An object to represent the JWE Compact Serialization. It is usually returned by
|
||||
``decrypt_compact`` method.
|
||||
"""
|
||||
|
||||
def __init__(self, protected: Header, plaintext: bytes | None = None):
|
||||
#: protected header in dict
|
||||
self.protected = protected
|
||||
#: the plaintext in bytes
|
||||
self.plaintext = plaintext
|
||||
self.recipient: Recipient[t.Any] | None = None
|
||||
self.bytes_segments: dict[str, bytes] = {} # store the decoded segments
|
||||
self.base64_segments: dict[str, bytes] = {} # store the encoded segments
|
||||
|
||||
def headers(self) -> Header:
|
||||
"""Returns the protected header values in dict."""
|
||||
return self.protected
|
||||
|
||||
def attach_recipient(self, key: Key, header: Header | None = None) -> None:
|
||||
"""Add a recipient to the JWE Compact Serialization. Please add a key that
|
||||
comply with the given "alg" value.
|
||||
|
||||
:param key: an instance of a key, e.g. (OctKey, RSAKey, ECKey, and etc)
|
||||
:param header: extra header in dict
|
||||
"""
|
||||
recipient = Recipient(self, None, key)
|
||||
if header:
|
||||
self.protected.update(header)
|
||||
self.recipient = recipient
|
||||
|
||||
@property
|
||||
def recipients(self) -> list[Recipient[t.Any]]:
|
||||
if self.recipient is not None:
|
||||
return [self.recipient]
|
||||
return []
|
||||
|
||||
|
||||
class BaseJSONEncryption(metaclass=ABCMeta):
|
||||
#: represents if the object is in flatten syntax
|
||||
flattened: t.ClassVar[bool]
|
||||
#: protected header in dict
|
||||
protected: Header
|
||||
#: the plaintext in bytes
|
||||
plaintext: t.Optional[bytes]
|
||||
#: unprotected header in dict
|
||||
unprotected: t.Optional[Header]
|
||||
#: an optional additional authenticated data
|
||||
aad: t.Optional[bytes]
|
||||
#: a list of recipients
|
||||
recipients: list[Recipient[t.Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protected: Header,
|
||||
plaintext: bytes | None = None,
|
||||
unprotected: Header | None = None,
|
||||
aad: bytes | None = None,
|
||||
):
|
||||
self.protected = protected
|
||||
self.plaintext = plaintext
|
||||
self.unprotected = unprotected
|
||||
self.aad = aad
|
||||
self.recipients = []
|
||||
self.bytes_segments: dict[str, bytes] = {} # store the decoded segments
|
||||
self.base64_segments: dict[str, bytes] = {} # store the encoded segments
|
||||
|
||||
@abstractmethod
|
||||
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
|
||||
"""Add a recipient to the JWE JSON Serialization. Please add a key that
|
||||
comply with the "alg" to this recipient.
|
||||
|
||||
:param header: recipient's own (unprotected) header
|
||||
:param key: an instance of a key, e.g. (OctKey, RSAKey, ECKey, and etc)
|
||||
"""
|
||||
|
||||
|
||||
class GeneralJSONEncryption(BaseJSONEncryption):
|
||||
"""An object to represent the JWE General JSON Serialization. It is used by
|
||||
``encrypt_json``, and it is usually returned by ``decrypt_json`` method.
|
||||
|
||||
To construct an object of ``GeneralJSONEncryption``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
protected = {"enc": "A128CBC-HS256"}
|
||||
plaintext = b"hello world"
|
||||
obj = GeneralJSONEncryption(protected, plaintext)
|
||||
# then add each recipient
|
||||
obj.add_recipient({"alg": "A128KW"})
|
||||
"""
|
||||
|
||||
flattened = False
|
||||
|
||||
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
|
||||
recipient = Recipient(self, header, key)
|
||||
if key and key.kid:
|
||||
recipient.set_kid(key.kid)
|
||||
self.recipients.append(recipient)
|
||||
|
||||
|
||||
class FlattenedJSONEncryption(BaseJSONEncryption):
|
||||
"""An object to represent the JWE Flattened JSON Serialization. It is used by
|
||||
``encrypt_json``, and it is usually returned by ``decrypt_json`` method.
|
||||
|
||||
To construct an object of ``FlattenedJSONEncryption``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
protected = {"enc": "A128CBC-HS256"}
|
||||
plaintext = b"hello world"
|
||||
obj = FlattenedJSONEncryption(protected, plaintext)
|
||||
# then add each recipient
|
||||
obj.add_recipient({"alg": "A128KW"})
|
||||
"""
|
||||
|
||||
flattened = True
|
||||
|
||||
def add_recipient(self, header: Header | None = None, key: Key | None = None) -> None:
|
||||
recipient = Recipient(self, header, key)
|
||||
if key and key.kid:
|
||||
recipient.set_kid(key.kid)
|
||||
self.recipients = [recipient]
|
||||
|
||||
|
||||
class JWEEncModel(metaclass=ABCMeta):
|
||||
name: str
|
||||
description: str
|
||||
recommended: bool = False
|
||||
algorithm_type: t.Literal["JWE"] = "JWE"
|
||||
algorithm_location: t.Literal["enc"] = "enc"
|
||||
|
||||
iv_size: int
|
||||
cek_size: int
|
||||
|
||||
def generate_cek(self) -> bytes:
|
||||
return secrets.token_bytes(self.cek_size // 8)
|
||||
|
||||
def generate_iv(self) -> bytes:
|
||||
return secrets.token_bytes(self.iv_size // 8)
|
||||
|
||||
def check_iv(self, iv: bytes) -> bytes:
|
||||
if len(iv) * 8 != self.iv_size: # pragma: no cover
|
||||
raise ValueError("Invalid 'iv' size")
|
||||
return iv
|
||||
|
||||
@abstractmethod
|
||||
def encrypt(self, plaintext: bytes, cek: bytes, iv: bytes, aad: bytes) -> tuple[bytes, bytes]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decrypt(self, ciphertext: bytes, tag: bytes, cek: bytes, iv: bytes, aad: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class JWEZipModel(metaclass=ABCMeta):
|
||||
name: str
|
||||
description: str
|
||||
recommended: bool = True
|
||||
algorithm_type: t.Literal["JWE"] = "JWE"
|
||||
algorithm_location: t.Literal["zip"] = "zip"
|
||||
|
||||
@abstractmethod
|
||||
def compress(self, s: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decompress(self, s: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class KeyManagement:
|
||||
name: str
|
||||
description: str
|
||||
recommended: bool = False
|
||||
key_size: int | None = None
|
||||
key_types: list[str]
|
||||
security_warning: str | None = None
|
||||
|
||||
algorithm_type: t.Literal["JWE"] = "JWE"
|
||||
algorithm_location: t.Literal["alg"] = "alg"
|
||||
more_header_registry: HeaderRegistryDict = {}
|
||||
|
||||
@property
|
||||
def direct_mode(self) -> bool:
|
||||
return self.key_size is None
|
||||
|
||||
def check_key_type(self, key: Key) -> None:
|
||||
if key.key_type not in self.key_types:
|
||||
raise InvalidKeyTypeError()
|
||||
|
||||
def prepare_recipient_header(self, recipient: Recipient[t.Any]) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class JWEDirectEncryption(KeyManagement, metaclass=ABCMeta):
|
||||
key_types = ["oct"]
|
||||
|
||||
@abstractmethod
|
||||
def compute_cek(self, size: int, recipient: Recipient[OctKey]) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class JWEKeyEncryption(KeyManagement, metaclass=ABCMeta):
|
||||
@property
|
||||
def direct_mode(self) -> bool:
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[t.Any]) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decrypt_cek(self, recipient: Recipient[t.Any]) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class JWEKeyWrapping(KeyManagement, metaclass=ABCMeta):
|
||||
key_size: int
|
||||
key_types = ["oct"]
|
||||
|
||||
@property
|
||||
def direct_mode(self) -> bool:
|
||||
return False
|
||||
|
||||
def check_op_key(self, op_key: bytes) -> None:
|
||||
if len(op_key) * 8 != self.key_size:
|
||||
raise InvalidKeyLengthError(f"A key of size {self.key_size} bits MUST be used")
|
||||
|
||||
@abstractmethod
|
||||
def wrap_cek(self, cek: bytes, key: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class JWEKeyAgreement(KeyManagement, metaclass=ABCMeta):
|
||||
key_types = ["EC", "OKP"]
|
||||
tag_aware: bool = False
|
||||
key_wrapping: t.Optional[JWEKeyWrapping]
|
||||
|
||||
def prepare_ephemeral_key(self, recipient: Recipient[ECKey]) -> None:
|
||||
recipient_key = recipient.recipient_key
|
||||
assert recipient_key is not None
|
||||
self.check_key_type(recipient_key)
|
||||
if recipient.ephemeral_key is None:
|
||||
ephemeral_key = recipient_key.generate_key(recipient_key.curve_name, private=True)
|
||||
recipient.ephemeral_key = ephemeral_key
|
||||
recipient.add_header("epk", recipient.ephemeral_key.as_dict(private=False))
|
||||
|
||||
@abstractmethod
|
||||
def encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
pass
|
||||
|
||||
def wrap_cek_with_auk(self, cek: bytes, key: bytes) -> bytes:
|
||||
assert self.key_wrapping is not None
|
||||
return self.key_wrapping.wrap_cek(cek, key)
|
||||
|
||||
def unwrap_cek_with_auk(self, ek: bytes, key: bytes) -> bytes:
|
||||
assert self.key_wrapping is not None
|
||||
return self.key_wrapping.unwrap_cek(ek, key)
|
||||
|
||||
def encrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
def decrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
JWEAlgModel = t.Union[JWEKeyEncryption, JWEKeyWrapping, JWEKeyAgreement, JWEDirectEncryption]
|
||||
@@ -0,0 +1,177 @@
|
||||
import warnings
|
||||
import typing as t
|
||||
from collections.abc import Collection
|
||||
from .models import JWEAlgModel, JWEEncModel, JWEZipModel
|
||||
from ..errors import (
|
||||
UnsupportedAlgorithmError,
|
||||
SecurityWarning,
|
||||
ExceededSizeError,
|
||||
)
|
||||
from ..registry import (
|
||||
Header,
|
||||
HeaderRegistryDict,
|
||||
JWE_HEADER_REGISTRY,
|
||||
check_supported_header,
|
||||
check_registry_header,
|
||||
check_crit_header,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"JWEAlgorithm",
|
||||
"JWERegistry",
|
||||
"default_registry",
|
||||
]
|
||||
|
||||
JWEAlgorithm = t.Union[JWEAlgModel, JWEEncModel, JWEZipModel]
|
||||
|
||||
AlgorithmsDict = t.TypedDict(
|
||||
"AlgorithmsDict",
|
||||
{
|
||||
"alg": dict[str, JWEAlgModel],
|
||||
"enc": dict[str, JWEEncModel],
|
||||
"zip": dict[str, JWEZipModel],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class JWERegistry:
|
||||
"""A registry for JSON Web Encryption to keep all the supported algorithms.
|
||||
An instance of ``JWERegistry`` is usually used together with methods in
|
||||
``joserfc.jwe``.
|
||||
|
||||
:param header_registry: extra header parameters registry
|
||||
:param algorithms: allowed algorithms to be used
|
||||
:param verify_all_recipients: validating all recipients in a JSON serialization
|
||||
:param strict_check_header: only allow header key in the registry to be used
|
||||
:param max_recipients: max number of recipients in JWE General JSON Serialization
|
||||
"""
|
||||
|
||||
algorithms: AlgorithmsDict = {
|
||||
"alg": {},
|
||||
"enc": {},
|
||||
"zip": {},
|
||||
}
|
||||
recommended: t.ClassVar[list[str]] = []
|
||||
|
||||
#: max protected header content's size in bytes
|
||||
max_protected_header_length: int = 1024
|
||||
#: max encrypted key's size in bytes
|
||||
max_encrypted_key_length: int = 1024
|
||||
#: max initialization vector's size in bytes
|
||||
max_initialization_vector_length: int = 64
|
||||
#: max ciphertext's size in bytes
|
||||
max_ciphertext_length: int = 65536 # 64KB
|
||||
#: max auth tag's size in bytes
|
||||
max_auth_tag_length: int = 64
|
||||
#: max number of recipients in JWE General JSON Serialization
|
||||
max_recipients: int = 16
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
header_registry: HeaderRegistryDict | None = None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
verify_all_recipients: bool = True,
|
||||
strict_check_header: bool = True,
|
||||
max_recipients: int | None = None,
|
||||
):
|
||||
self.header_registry: HeaderRegistryDict = {}
|
||||
self.header_registry.update(JWE_HEADER_REGISTRY)
|
||||
if header_registry is not None:
|
||||
self.header_registry.update(header_registry)
|
||||
self.allowed = algorithms
|
||||
self.verify_all_recipients = verify_all_recipients
|
||||
self.strict_check_header = strict_check_header
|
||||
if max_recipients is not None:
|
||||
self.max_recipients = max_recipients
|
||||
|
||||
@classmethod
|
||||
def register(cls, model: JWEAlgorithm) -> None:
|
||||
cls.algorithms[model.algorithm_location][model.name] = model # type: ignore
|
||||
if model.recommended:
|
||||
cls.recommended.append(model.name)
|
||||
|
||||
def check_header(self, header: Header, check_more: bool = False) -> 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)
|
||||
|
||||
alg = self.get_alg(header["alg"])
|
||||
if alg.more_header_registry:
|
||||
check_registry_header(alg.more_header_registry, header, check_more)
|
||||
|
||||
if self.strict_check_header:
|
||||
allowed_registry = self.header_registry.copy()
|
||||
allowed_registry.update(alg.more_header_registry)
|
||||
check_supported_header(allowed_registry, header)
|
||||
elif self.strict_check_header:
|
||||
check_supported_header(self.header_registry, header)
|
||||
|
||||
def validate_protected_header_size(self, header: bytes) -> None:
|
||||
if header and len(header) > self.max_protected_header_length:
|
||||
raise ExceededSizeError(f"Header size exceeds {self.max_protected_header_length} bytes.")
|
||||
|
||||
def validate_encrypted_key_size(self, ek: bytes) -> None:
|
||||
if ek and len(ek) > self.max_encrypted_key_length:
|
||||
raise ExceededSizeError(f"Encrypted key size exceeds {self.max_encrypted_key_length} bytes.")
|
||||
|
||||
def validate_initialization_vector_size(self, iv: bytes) -> None:
|
||||
if iv and len(iv) > self.max_initialization_vector_length:
|
||||
raise ExceededSizeError(
|
||||
f"Initialization vector size exceeds {self.max_initialization_vector_length} bytes."
|
||||
)
|
||||
|
||||
def validate_ciphertext_size(self, ciphertext: bytes) -> None:
|
||||
if ciphertext and len(ciphertext) > self.max_ciphertext_length:
|
||||
raise ExceededSizeError(f"Ciphertext size exceeds {self.max_ciphertext_length} bytes.")
|
||||
|
||||
def validate_auth_tag_size(self, tag: bytes) -> None:
|
||||
if tag and len(tag) > self.max_auth_tag_length:
|
||||
raise ExceededSizeError(f"Auth tag size exceeds {self.max_auth_tag_length} bytes.")
|
||||
|
||||
def validate_recipient_count(self, recipients: Collection[t.Any]) -> None:
|
||||
if len(recipients) > self.max_recipients:
|
||||
raise ExceededSizeError(f"Recipient count exceeds {self.max_recipients}.")
|
||||
|
||||
def get_alg(self, name: str) -> JWEAlgModel:
|
||||
"""Get the allowed ("alg") algorithm instance of the given name.
|
||||
|
||||
:param name: value of the ``alg``, e.g. ``ECDH-ES``, ``A128KW``
|
||||
"""
|
||||
registry = self.algorithms["alg"]
|
||||
self._check_algorithm(name, registry)
|
||||
alg: JWEAlgModel = registry[name]
|
||||
if alg.security_warning:
|
||||
warnings.warn(alg.security_warning, SecurityWarning)
|
||||
return alg
|
||||
|
||||
def get_enc(self, name: str) -> JWEEncModel:
|
||||
"""Get the allowed ("enc") algorithm instance of the given name.
|
||||
|
||||
:param name: value of the ``enc``, e.g. ``A128CBC-HS256``, ``A128GCM``
|
||||
"""
|
||||
registry = self.algorithms["enc"]
|
||||
self._check_algorithm(name, registry)
|
||||
return registry[name]
|
||||
|
||||
def get_zip(self, name: str) -> JWEZipModel:
|
||||
"""Get the allowed ("zip") algorithm instance of the given name.
|
||||
|
||||
:param name: value of the ``zip``, e.g. ``DEF``
|
||||
"""
|
||||
registry = self.algorithms["zip"]
|
||||
self._check_algorithm(name, registry)
|
||||
return registry[name]
|
||||
|
||||
def _check_algorithm(self, name: str, registry: dict[str, t.Any]) -> None:
|
||||
if name not in registry:
|
||||
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")
|
||||
|
||||
|
||||
default_registry = JWERegistry()
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import Any, TypedDict
|
||||
|
||||
__all__ = [
|
||||
"JSONRecipientDict",
|
||||
"FlattenedJSONSerialization",
|
||||
"GeneralJSONSerialization",
|
||||
]
|
||||
|
||||
|
||||
class JSONRecipientDict(TypedDict, total=False):
|
||||
header: dict[str, Any]
|
||||
encrypted_key: str
|
||||
|
||||
|
||||
class GeneralJSONSerialization(TypedDict, total=False):
|
||||
protected: str
|
||||
unprotected: dict[str, Any]
|
||||
iv: str
|
||||
aad: str
|
||||
ciphertext: str
|
||||
tag: str
|
||||
recipients: list[JSONRecipientDict]
|
||||
|
||||
|
||||
class FlattenedJSONSerialization(TypedDict, total=False):
|
||||
protected: str
|
||||
unprotected: dict[str, Any]
|
||||
iv: str
|
||||
aad: str
|
||||
ciphertext: str
|
||||
tag: str
|
||||
header: dict[str, Any]
|
||||
encrypted_key: str
|
||||
@@ -0,0 +1,345 @@
|
||||
import typing as t
|
||||
from collections.abc import KeysView
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from .types import DictKey, AnyKey, KeyParameters
|
||||
from .._rfc7638 import calculate_thumbprint
|
||||
from .._rfc9278 import concat_thumbprint_uri
|
||||
from ..registry import (
|
||||
KeyParameterRegistryDict,
|
||||
JWK_PARAMETER_REGISTRY,
|
||||
KeyOperationRegistryDict,
|
||||
JWK_OPERATION_REGISTRY,
|
||||
)
|
||||
from ..util import to_bytes
|
||||
from ..errors import (
|
||||
KeyParameterError,
|
||||
UnsupportedKeyUseError,
|
||||
UnsupportedKeyAlgorithmError,
|
||||
UnsupportedKeyOperationError,
|
||||
)
|
||||
|
||||
|
||||
NativePrivateKey = t.TypeVar("NativePrivateKey")
|
||||
NativePublicKey = t.TypeVar("NativePublicKey")
|
||||
GenericKey = t.TypeVar("GenericKey", bound="BaseKey[t.Any, t.Any]")
|
||||
|
||||
|
||||
class NativeKeyBinding(metaclass=ABCMeta):
|
||||
use_key_ops_registry: t.ClassVar[dict[str, list[str]]] = {
|
||||
"sig": ["sign", "verify"],
|
||||
"enc": ["encrypt", "decrypt", "wrapKey", "unwrapKey", "deriveKey", "deriveBits"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def convert_raw_key_to_dict(cls, raw_key: t.Any, private: bool) -> DictKey:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_from_dict(cls, value: DictKey) -> t.Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_from_bytes(cls, value: bytes, password: t.Any = None) -> t.Any:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def as_bytes(
|
||||
key: GenericKey,
|
||||
encoding: t.Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: str | None = None,
|
||||
) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key_registry(cls, dict_key: DictKey, registry: KeyParameterRegistryDict) -> None:
|
||||
for k in registry:
|
||||
if registry[k].required and k not in dict_key:
|
||||
raise KeyParameterError(f"'{k}' is required")
|
||||
|
||||
if k in dict_key:
|
||||
try:
|
||||
registry[k].validate(dict_key[k])
|
||||
except ValueError as error:
|
||||
raise KeyParameterError(f"'{k}' {error}")
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key_use_operations(cls, dict_key: DictKey) -> None:
|
||||
if "use" in dict_key and "key_ops" in dict_key:
|
||||
_use: str = dict_key["use"] # type: ignore
|
||||
operations = cls.use_key_ops_registry[_use]
|
||||
for op in dict_key["key_ops"]:
|
||||
if op not in operations:
|
||||
raise KeyParameterError("'use' and 'key_ops' does not match")
|
||||
|
||||
|
||||
class BaseKey(t.Generic[NativePrivateKey, NativePublicKey], metaclass=ABCMeta):
|
||||
key_type: t.ClassVar[str]
|
||||
binding: t.ClassVar[type[NativeKeyBinding]]
|
||||
value_registry: t.ClassVar[KeyParameterRegistryDict]
|
||||
param_registry: t.ClassVar[KeyParameterRegistryDict] = JWK_PARAMETER_REGISTRY
|
||||
operation_registry: t.ClassVar[KeyOperationRegistryDict] = JWK_OPERATION_REGISTRY
|
||||
thumbprint_digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_value: NativePrivateKey | NativePublicKey,
|
||||
original_value: t.Any,
|
||||
parameters: KeyParameters | None = None,
|
||||
):
|
||||
self._raw_value = raw_value
|
||||
self.original_value = original_value
|
||||
self.extra_parameters = parameters
|
||||
self._dict_value: DictKey = {}
|
||||
if isinstance(original_value, dict):
|
||||
if parameters is not None:
|
||||
data = {**original_value, **parameters, "kty": self.key_type}
|
||||
else:
|
||||
data = {**original_value, "kty": self.key_type}
|
||||
self.validate_dict_key(data)
|
||||
self._dict_value = data
|
||||
|
||||
def __eq__(self, other: t.Any) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.dict_value == other.dict_value
|
||||
|
||||
def keys(self) -> KeysView[str]:
|
||||
return self.dict_value.keys()
|
||||
|
||||
def __getitem__(self, k: str) -> str | list[str]:
|
||||
return self.dict_value[k]
|
||||
|
||||
def get(self, k: str, default: str | list[str] | None = None) -> str | list[str] | None:
|
||||
return self.dict_value.get(k, default)
|
||||
|
||||
def ensure_kid(self) -> None:
|
||||
"""Ensure this key has a ``kid``. If ``kid`` is not provided by default,
|
||||
it will generate the kid with ``.thumbprint`` method, which is defined
|
||||
by RFC7638."""
|
||||
if "kid" not in self.dict_value:
|
||||
self._dict_value["kid"] = self.thumbprint()
|
||||
|
||||
@property
|
||||
def kid(self) -> str | None:
|
||||
"""The "kid" value of the JSON Web Key."""
|
||||
return t.cast(t.Optional[str], self.get("kid"))
|
||||
|
||||
@property
|
||||
def alg(self) -> str | None:
|
||||
"""The "alg" value of the JSON Web Key."""
|
||||
return t.cast(t.Optional[str], self.get("alg"))
|
||||
|
||||
@property
|
||||
def raw_value(self) -> t.Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def dict_value(self) -> DictKey:
|
||||
"""Property of the Key in Dict (JSON)."""
|
||||
if self._dict_value:
|
||||
return self._dict_value
|
||||
|
||||
data = self.binding.convert_raw_key_to_dict(self.raw_value, self.is_private)
|
||||
if self.extra_parameters is not None:
|
||||
data.update(self.extra_parameters) # type: ignore
|
||||
data["kty"] = self.key_type
|
||||
self.validate_dict_key(data)
|
||||
self._dict_value = data
|
||||
return data
|
||||
|
||||
@property
|
||||
def public_key(self) -> NativePublicKey:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def private_key(self) -> NativePrivateKey | None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def thumbprint(self) -> str:
|
||||
"""Call this method will generate the thumbprint with algorithm
|
||||
defined in RFC7638."""
|
||||
fields = [k for k in self.value_registry if self.value_registry[k].required]
|
||||
fields.append("kty")
|
||||
data = {key: self.dict_value[key] for key in fields}
|
||||
return calculate_thumbprint(data, self.thumbprint_digest_method)
|
||||
|
||||
def thumbprint_uri(self) -> str:
|
||||
"""Call this method will generate the thumbprint URI
|
||||
defined in RFC9278."""
|
||||
value = self.thumbprint()
|
||||
return concat_thumbprint_uri(value, self.thumbprint_digest_method)
|
||||
|
||||
def as_dict(self, private: bool = False, **params: t.Any) -> DictKey:
|
||||
"""Output this key to a JWK format (in dict). By default, it will return
|
||||
the ``dict_value`` of this key.
|
||||
|
||||
:param private: determine whether this method should output private key or not
|
||||
:param params: other parameters added into this key
|
||||
:raise: ValueError
|
||||
"""
|
||||
# check private conflicts
|
||||
if private and not self.is_private:
|
||||
raise ValueError("This key is not a private key.")
|
||||
|
||||
data = self.dict_value.copy()
|
||||
if private:
|
||||
data.update(params)
|
||||
return data
|
||||
|
||||
# clear private fields
|
||||
for k in self.dict_value:
|
||||
if k in self.value_registry and self.value_registry[k].private:
|
||||
del data[k]
|
||||
|
||||
data.update(params)
|
||||
return data
|
||||
|
||||
def check_use(self, use: str) -> None:
|
||||
"""Check if this key supports the given "use".
|
||||
|
||||
Values defined by this specification are:
|
||||
|
||||
- "sig" (signature)
|
||||
- "enc" (encryption)
|
||||
|
||||
Other values MAY be used. The "use" value is a case-sensitive
|
||||
string. Use of the "use" member is OPTIONAL, unless the application
|
||||
requires its presence.
|
||||
|
||||
:param use: this key is used for, e.g. "sig", "enc"
|
||||
:raise UnsupportedKeyUseError: if this key is not designed for the given use
|
||||
"""
|
||||
designed_use = self.get("use")
|
||||
if designed_use and designed_use != use:
|
||||
raise UnsupportedKeyUseError(f"This key is designed to be used for '{designed_use}'")
|
||||
|
||||
def check_alg(self, alg: str) -> None:
|
||||
"""Check if this key supports the given "alg".
|
||||
|
||||
:param alg: the algorithm this key is intended to be used, e.g. "HS256", "ECDH-EC"
|
||||
:raise UnsupportedKeyAlgorithmError: if this key is not designed for the given algorithm
|
||||
"""
|
||||
designed_alg = self.get("alg")
|
||||
if designed_alg and designed_alg != alg:
|
||||
raise UnsupportedKeyAlgorithmError(f"This key is designed for algorithm '{designed_alg}'")
|
||||
|
||||
def check_key_op(self, operation: str) -> None:
|
||||
"""Check if the given key_op is supported by this key.
|
||||
|
||||
:param operation: key operation value, such as "sign", "encrypt".
|
||||
:raise UnsupportedKeyOperationError: if the operation is not supported by this key.
|
||||
"""
|
||||
key_ops = self.get("key_ops")
|
||||
if key_ops is not None and operation not in key_ops:
|
||||
raise UnsupportedKeyOperationError(f"Unsupported key_op '{operation}'")
|
||||
|
||||
assert operation in self.operation_registry
|
||||
reg = self.operation_registry[operation]
|
||||
if reg.private and not self.is_private:
|
||||
raise UnsupportedKeyOperationError(f"Invalid key_op '{operation}' for public key")
|
||||
|
||||
@t.overload
|
||||
def get_op_key(self, operation: t.Literal["verify", "encrypt", "wrapKey", "deriveKey"]) -> NativePublicKey: ...
|
||||
|
||||
@t.overload
|
||||
def get_op_key(self, operation: t.Literal["sign", "decrypt", "unwrapKey"]) -> NativePrivateKey: ...
|
||||
|
||||
def get_op_key(self, operation: str) -> NativePublicKey | NativePrivateKey:
|
||||
self.check_key_op(operation)
|
||||
reg = self.operation_registry[operation]
|
||||
if reg.private:
|
||||
assert self.private_key is not None
|
||||
return self.private_key
|
||||
return self.public_key
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key(cls, data: DictKey) -> None:
|
||||
cls.binding.validate_dict_key_registry(data, cls.param_registry)
|
||||
cls.binding.validate_dict_key_registry(data, cls.value_registry)
|
||||
cls.binding.validate_dict_key_use_operations(data)
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: type[GenericKey],
|
||||
value: AnyKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: t.Any = None,
|
||||
) -> GenericKey:
|
||||
if isinstance(value, dict):
|
||||
cls.validate_dict_key(value)
|
||||
raw_key = cls.binding.import_from_dict(value)
|
||||
return cls(raw_key, value, parameters)
|
||||
|
||||
raw_key = cls.binding.import_from_bytes(to_bytes(value), password)
|
||||
return cls(raw_key, value, parameters)
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls: type[GenericKey],
|
||||
*,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> GenericKey:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SymmetricKey(BaseKey[bytes, bytes], metaclass=ABCMeta):
|
||||
@property
|
||||
def raw_value(self) -> bytes:
|
||||
"""The raw key in bytes."""
|
||||
return self._raw_value
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
"""A symmetric key will always be private."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def public_key(self) -> bytes:
|
||||
"""Returns the ``raw_value`` as the public key."""
|
||||
return self.raw_value
|
||||
|
||||
@property
|
||||
def private_key(self) -> bytes:
|
||||
"""Returns the ``raw_value`` as the private key."""
|
||||
return self.raw_value
|
||||
|
||||
|
||||
class AsymmetricKey(BaseKey[NativePrivateKey, NativePublicKey], metaclass=ABCMeta):
|
||||
@property
|
||||
def raw_value(self) -> t.Union[NativePublicKey, NativePrivateKey]:
|
||||
return self._raw_value
|
||||
|
||||
def as_bytes(
|
||||
self,
|
||||
encoding: t.Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: str | None = None,
|
||||
) -> bytes:
|
||||
return self.binding.as_bytes(self, encoding, private, password)
|
||||
|
||||
def as_pem(self, private: bool = False, password: str | None = None) -> bytes:
|
||||
return self.as_bytes(private=private, password=password)
|
||||
|
||||
def as_der(self, private: bool = False, password: str | None = None) -> bytes:
|
||||
return self.as_bytes(encoding="DER", private=private, password=password)
|
||||
|
||||
|
||||
class CurveKey(AsymmetricKey[NativePrivateKey, NativePublicKey]):
|
||||
@property
|
||||
@abstractmethod
|
||||
def curve_name(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exchange_derive_key(self, key: t.Any) -> bytes:
|
||||
pass
|
||||
@@ -0,0 +1,163 @@
|
||||
from typing import Any, Literal, cast
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
load_ssh_public_key,
|
||||
load_ssh_private_key,
|
||||
load_der_private_key,
|
||||
load_der_public_key,
|
||||
Encoding,
|
||||
PrivateFormat,
|
||||
PublicFormat,
|
||||
KeySerializationEncryption,
|
||||
BestAvailableEncryption,
|
||||
NoEncryption,
|
||||
)
|
||||
from .models import NativeKeyBinding, GenericKey
|
||||
from .types import DictKey
|
||||
from ..errors import InvalidKeyTypeError
|
||||
from ..util import to_bytes
|
||||
|
||||
|
||||
def import_from_ssh_key(raw: bytes) -> Any:
|
||||
return load_ssh_public_key(raw)
|
||||
|
||||
|
||||
def import_from_pem_key(raw: bytes, password: bytes | None = None) -> Any:
|
||||
key: Any
|
||||
|
||||
if b"OPENSSH PRIVATE" in raw:
|
||||
key = load_ssh_private_key(raw, password=password)
|
||||
|
||||
elif b"PUBLIC" in raw:
|
||||
key = load_pem_public_key(raw)
|
||||
|
||||
elif b"PRIVATE" in raw:
|
||||
key = load_pem_private_key(raw, password=password)
|
||||
|
||||
elif b"CERTIFICATE" in raw:
|
||||
cert = load_pem_x509_certificate(raw)
|
||||
return cert.public_key()
|
||||
|
||||
else:
|
||||
try:
|
||||
key = load_der_private_key(raw, password=password)
|
||||
except ValueError:
|
||||
key = load_der_public_key(raw)
|
||||
return key
|
||||
|
||||
|
||||
def dump_pem_key(
|
||||
key: Any,
|
||||
encoding: Literal["PEM", "DER"] | None = None,
|
||||
private: bool | None = False,
|
||||
password: Any | None = None,
|
||||
) -> bytes:
|
||||
"""Export key into PEM/DER format bytes.
|
||||
|
||||
:param key: native cryptography key
|
||||
:param encoding: "PEM" or "DER"
|
||||
:param private: export private key or public key
|
||||
:param password: encrypt private key with password
|
||||
:return: bytes
|
||||
"""
|
||||
|
||||
if encoding is None or encoding == "PEM":
|
||||
encoding_enum = Encoding.PEM
|
||||
elif encoding == "DER":
|
||||
encoding_enum = Encoding.DER
|
||||
else: # pragma: no cover
|
||||
raise ValueError(f"Invalid encoding: {encoding}")
|
||||
|
||||
if private:
|
||||
encryption_algorithm: KeySerializationEncryption
|
||||
if password is None:
|
||||
encryption_algorithm = NoEncryption()
|
||||
else:
|
||||
encryption_algorithm = BestAvailableEncryption(to_bytes(password))
|
||||
value = key.private_bytes(
|
||||
encoding=encoding_enum,
|
||||
format=PrivateFormat.PKCS8,
|
||||
encryption_algorithm=encryption_algorithm,
|
||||
)
|
||||
else:
|
||||
value = key.public_bytes(
|
||||
encoding=encoding_enum,
|
||||
format=PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return cast(bytes, value)
|
||||
|
||||
|
||||
class CryptographyBinding(NativeKeyBinding, metaclass=ABCMeta):
|
||||
key_type: str
|
||||
ssh_type: bytes
|
||||
_cryptography_key_types: Any
|
||||
|
||||
@classmethod
|
||||
def check_ssh_type(cls, value: bytes) -> bool:
|
||||
return value.startswith(cls.ssh_type)
|
||||
|
||||
@classmethod
|
||||
def check_cryptography_key(cls, native_key: Any) -> bool:
|
||||
return isinstance(native_key, cls._cryptography_key_types)
|
||||
|
||||
@classmethod
|
||||
def convert_raw_key_to_dict(cls, raw_key: Any, private: bool) -> DictKey:
|
||||
if private:
|
||||
value = cls.export_private_key(raw_key)
|
||||
else:
|
||||
value = cls.export_public_key(raw_key)
|
||||
return cast(DictKey, value)
|
||||
|
||||
@classmethod
|
||||
def import_from_dict(cls, value: DictKey) -> Any:
|
||||
if "d" in value:
|
||||
return cls.import_private_key(value)
|
||||
return cls.import_public_key(value)
|
||||
|
||||
@classmethod
|
||||
def import_from_bytes(cls, value: bytes, password: Any | None = None) -> Any:
|
||||
if cls.check_ssh_type(value):
|
||||
return import_from_ssh_key(value)
|
||||
|
||||
if password is not None:
|
||||
password = to_bytes(password)
|
||||
|
||||
key = import_from_pem_key(value, password)
|
||||
if not cls.check_cryptography_key(key):
|
||||
raise InvalidKeyTypeError(f"Not a key of: '{cls.key_type}'")
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def as_bytes(
|
||||
key: GenericKey,
|
||||
encoding: Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: Any | None = None,
|
||||
) -> bytes:
|
||||
if private:
|
||||
return dump_pem_key(key.private_key, encoding, private, password)
|
||||
else:
|
||||
return dump_pem_key(key.public_key, encoding, private, password)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_private_key(cls, obj: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_public_key(cls, obj: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def export_private_key(cls, key: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def export_public_key(cls, key: Any) -> Any:
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
import typing as t
|
||||
|
||||
__all__ = ["DictKey", "AnyKey", "KeyParameters"]
|
||||
|
||||
#: JSON Web Key in dict
|
||||
DictKey = dict[str, t.Union[str, list[str]]]
|
||||
|
||||
#: Key in str, bytes and dict
|
||||
AnyKey = t.Union[str, bytes, DictKey]
|
||||
|
||||
#: extra key parameters for JWK
|
||||
KeyParameters = t.TypedDict(
|
||||
"KeyParameters",
|
||||
{
|
||||
"use": str,
|
||||
"key_ops": list[str],
|
||||
"alg": str,
|
||||
"kid": str,
|
||||
"x5u": str,
|
||||
"x5c": list[str],
|
||||
"x5t": str,
|
||||
"x5t#S256": str,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import struct
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
|
||||
from ..registry import Header
|
||||
from ..util import to_bytes, urlsafe_b64decode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"derive_key_for_concat_kdf",
|
||||
"u32be_len_input",
|
||||
]
|
||||
|
||||
|
||||
def derive_key_for_concat_kdf(
|
||||
shared_key: bytes, header: Header, cek_size: int, key_size: int | None, tag: bytes | None = None
|
||||
) -> bytes:
|
||||
# PartyUInfo
|
||||
apu_info = u32be_len_input(header.get("apu"), True)
|
||||
# PartyVInfo
|
||||
apv_info = u32be_len_input(header.get("apv"), True)
|
||||
# SuppPubInfo
|
||||
|
||||
if key_size:
|
||||
alg_id = u32be_len_input(header["alg"])
|
||||
bit_size = key_size
|
||||
else:
|
||||
alg_id = u32be_len_input(header["enc"])
|
||||
bit_size = cek_size
|
||||
|
||||
pub_info = struct.pack(">I", bit_size)
|
||||
fixed_info = alg_id + apu_info + apv_info + pub_info
|
||||
|
||||
if tag:
|
||||
cctag = u32be_len_input(tag)
|
||||
fixed_info += cctag
|
||||
|
||||
ckdf = ConcatKDFHash(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=bit_size // 8,
|
||||
otherinfo=fixed_info,
|
||||
)
|
||||
return ckdf.derive(shared_key)
|
||||
|
||||
|
||||
def u32be_len_input(s: bytes | str | None, use_base64: bool = False) -> bytes:
|
||||
if not s:
|
||||
return b"\x00\x00\x00\x00"
|
||||
sb: bytes
|
||||
if use_base64:
|
||||
sb = urlsafe_b64decode(to_bytes(s))
|
||||
else:
|
||||
sb = to_bytes(s)
|
||||
return struct.pack(">I", len(sb)) + sb
|
||||
@@ -0,0 +1,266 @@
|
||||
import typing as t
|
||||
from functools import cached_property
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
derive_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePublicKey,
|
||||
EllipticCurvePrivateKey,
|
||||
EllipticCurvePrivateNumbers,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurve,
|
||||
SECP256R1,
|
||||
SECP384R1,
|
||||
SECP521R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from ..errors import InvalidExchangeKeyError, InvalidKeyCurveError
|
||||
from .._rfc7517.models import CurveKey
|
||||
from .._rfc7517.pem import CryptographyBinding
|
||||
from .._rfc7517.types import KeyParameters, AnyKey
|
||||
from ..util import base64_to_int, int_to_base64, to_bytes
|
||||
from ..registry import KeyParameter
|
||||
|
||||
__all__ = ["ECKey"]
|
||||
|
||||
ECDictKey = t.TypedDict(
|
||||
"ECDictKey",
|
||||
{
|
||||
"crv": str,
|
||||
"x": str,
|
||||
"y": str,
|
||||
"d": str, # optional
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
|
||||
|
||||
class ECBinding(CryptographyBinding):
|
||||
key_type = "EC"
|
||||
ssh_type = b"ecdsa-sha2-"
|
||||
_cryptography_key_types = (EllipticCurvePrivateKey, EllipticCurvePublicKey)
|
||||
|
||||
_dss_curves: dict[str, type[EllipticCurve]] = {}
|
||||
_curves_dss: dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def register_curve(cls, name: str, curve: type[EllipticCurve]) -> None:
|
||||
cls._dss_curves[name] = curve
|
||||
cls._curves_dss[str(curve.name)] = name
|
||||
|
||||
@classmethod
|
||||
def generate_private_key(cls, name: str) -> EllipticCurvePrivateKey:
|
||||
if name not in cls._dss_curves:
|
||||
raise InvalidKeyCurveError(f"Invalid crv value: '{name}'")
|
||||
|
||||
curve = cls._dss_curves[name]()
|
||||
return generate_private_key(curve=curve)
|
||||
|
||||
@classmethod
|
||||
def import_private_key(cls, obj: ECDictKey) -> EllipticCurvePrivateKey:
|
||||
curve = cls._dss_curves[obj["crv"]]()
|
||||
public_numbers = EllipticCurvePublicNumbers(
|
||||
base64_to_int(obj["x"]),
|
||||
base64_to_int(obj["y"]),
|
||||
curve,
|
||||
)
|
||||
d = base64_to_int(obj["d"])
|
||||
private_numbers = EllipticCurvePrivateNumbers(d, public_numbers)
|
||||
return private_numbers.private_key()
|
||||
|
||||
@classmethod
|
||||
def export_private_key(cls, key: EllipticCurvePrivateKey) -> ECDictKey:
|
||||
numbers = key.private_numbers()
|
||||
byte_count = (key.key_size + 7) // 8
|
||||
return {
|
||||
"crv": cls._curves_dss[key.curve.name],
|
||||
"x": int_to_base64(numbers.public_numbers.x, byte_count),
|
||||
"y": int_to_base64(numbers.public_numbers.y, byte_count),
|
||||
"d": int_to_base64(numbers.private_value, byte_count),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def import_public_key(cls, obj: ECDictKey) -> EllipticCurvePublicKey:
|
||||
curve = cls._dss_curves[obj["crv"]]()
|
||||
public_numbers = EllipticCurvePublicNumbers(
|
||||
base64_to_int(obj["x"]),
|
||||
base64_to_int(obj["y"]),
|
||||
curve,
|
||||
)
|
||||
return public_numbers.public_key()
|
||||
|
||||
@classmethod
|
||||
def export_public_key(cls, key: EllipticCurvePublicKey) -> ECDictKey:
|
||||
numbers = key.public_numbers()
|
||||
byte_count = (key.key_size + 7) // 8
|
||||
return {
|
||||
"crv": cls._curves_dss[numbers.curve.name],
|
||||
"x": int_to_base64(numbers.x, byte_count),
|
||||
"y": int_to_base64(numbers.y, byte_count),
|
||||
}
|
||||
|
||||
|
||||
# register default curves with their DSS (Digital Signature Standard) names
|
||||
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
|
||||
ECBinding.register_curve("P-256", SECP256R1)
|
||||
ECBinding.register_curve("P-384", SECP384R1)
|
||||
ECBinding.register_curve("P-521", SECP521R1)
|
||||
|
||||
|
||||
class ECKey(CurveKey[EllipticCurvePrivateKey, EllipticCurvePublicKey]):
|
||||
key_type = "EC"
|
||||
#: Registry definition for EC Key
|
||||
#: https://www.rfc-editor.org/rfc/rfc7518#section-6.2
|
||||
value_registry = {
|
||||
"crv": KeyParameter("Curve", "str", private=False, required=True),
|
||||
"x": KeyParameter("X Coordinate", "str", private=False, required=True),
|
||||
"y": KeyParameter("Y Coordinate", "str", private=False, required=True),
|
||||
"d": KeyParameter("EC Private Key", "str", private=True, required=False),
|
||||
}
|
||||
binding = ECBinding
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
return isinstance(self.raw_value, EllipticCurvePrivateKey)
|
||||
|
||||
@cached_property
|
||||
def public_key(self) -> EllipticCurvePublicKey:
|
||||
if isinstance(self.raw_value, EllipticCurvePrivateKey):
|
||||
return self.raw_value.public_key()
|
||||
return self.raw_value
|
||||
|
||||
@property
|
||||
def private_key(self) -> EllipticCurvePrivateKey | None:
|
||||
if isinstance(self.raw_value, EllipticCurvePrivateKey):
|
||||
return self.raw_value
|
||||
return None
|
||||
|
||||
def exchange_derive_key(self, key: "ECKey") -> bytes:
|
||||
pubkey = key.get_op_key("deriveKey")
|
||||
if self.private_key and self.curve_name == key.curve_name:
|
||||
return self.private_key.exchange(ECDH(), pubkey)
|
||||
raise InvalidExchangeKeyError()
|
||||
|
||||
@property
|
||||
def curve_name(self) -> str:
|
||||
return self.binding._curves_dss[self.raw_value.curve.name]
|
||||
|
||||
@property
|
||||
def curve_key_size(self) -> int:
|
||||
return self.raw_value.curve.key_size
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: t.Any,
|
||||
value: AnyKey | EllipticCurvePrivateKey | EllipticCurvePublicKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: t.Any = None,
|
||||
) -> "ECKey":
|
||||
key: ECKey
|
||||
if isinstance(value, (EllipticCurvePrivateKey, EllipticCurvePublicKey)):
|
||||
key = cls(value, value, parameters)
|
||||
else:
|
||||
key = super(ECKey, cls).import_key(value, parameters, password)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls: type["ECKey"],
|
||||
crv: str | None = "P-256",
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> "ECKey":
|
||||
"""Generate a ``ECKey`` with the given "crv" value.
|
||||
|
||||
:param crv: ECKey curve name
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: generate a private key or public key
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
"""
|
||||
if crv is None:
|
||||
crv = "P-256"
|
||||
raw_key = cls.binding.generate_private_key(crv)
|
||||
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
|
||||
|
||||
@classmethod
|
||||
def derive_key(
|
||||
cls: type["ECKey"],
|
||||
secret: bytes | str,
|
||||
crv: str = "P-256",
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
kdf_name: t.Literal["HKDF", "PBKDF2"] = "HKDF",
|
||||
kdf_options: dict[str, t.Any] | None = None,
|
||||
) -> "ECKey":
|
||||
"""
|
||||
Generate an elliptic curve cryptographic key derived from a secret input using a key
|
||||
derivation function (KDF). This allows the creation of deterministic elliptic curve
|
||||
keys based on given input data, curve specification, and KDF options.
|
||||
|
||||
:param secret: The input secret used for key derivation
|
||||
:param crv: ECKey curve name
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: generate a private key or public key
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
:param kdf_name: Key derivation function name
|
||||
:param kdf_options: Additional options for the KDF
|
||||
"""
|
||||
try:
|
||||
curve_class = cls.binding._dss_curves[crv]
|
||||
except KeyError:
|
||||
raise InvalidKeyCurveError(f"Invalid crv value: '{crv}'")
|
||||
|
||||
curve = curve_class()
|
||||
length = (curve.group_order.bit_length() + 7) // 8 * 2
|
||||
|
||||
if kdf_options is None:
|
||||
kdf_options = {}
|
||||
|
||||
algorithm = kdf_options.pop("algorithm", None)
|
||||
if algorithm is None:
|
||||
algorithm = hashes.SHA256()
|
||||
|
||||
kdf_options.setdefault("salt", to_bytes(f"joserfc:EC:{kdf_name}:{crv}"))
|
||||
if kdf_name == "HKDF":
|
||||
kdf_options.setdefault("info", b"")
|
||||
hkdf = HKDF(
|
||||
algorithm=algorithm,
|
||||
length=length,
|
||||
**kdf_options,
|
||||
)
|
||||
seed = hkdf.derive(to_bytes(secret))
|
||||
elif kdf_name == "PBKDF2":
|
||||
kdf_options.setdefault("iterations", 100000)
|
||||
pbkdf2 = PBKDF2HMAC(
|
||||
algorithm=algorithm,
|
||||
length=length,
|
||||
**kdf_options,
|
||||
)
|
||||
seed = pbkdf2.derive(to_bytes(secret))
|
||||
else:
|
||||
raise ValueError(f"Invalid kdf value: '{kdf_name}'")
|
||||
|
||||
d = int.from_bytes(seed, "big") % curve.group_order
|
||||
raw_key = derive_private_key(d, curve)
|
||||
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
|
||||
|
||||
|
||||
def _wrap_key(
|
||||
cls: type["ECKey"],
|
||||
raw_key: EllipticCurvePrivateKey,
|
||||
private: bool,
|
||||
auto_kid: bool,
|
||||
parameters: KeyParameters | None = None,
|
||||
) -> ECKey:
|
||||
if private:
|
||||
key = cls(raw_key, raw_key, parameters)
|
||||
else:
|
||||
pub_key = raw_key.public_key()
|
||||
key = cls(pub_key, pub_key, parameters)
|
||||
if auto_kid:
|
||||
key.ensure_kid()
|
||||
return key
|
||||
@@ -0,0 +1,351 @@
|
||||
import secrets
|
||||
import warnings
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.keywrap import (
|
||||
aes_key_wrap,
|
||||
aes_key_unwrap,
|
||||
InvalidUnwrap,
|
||||
)
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.exceptions import InvalidTag
|
||||
from .derive_key import derive_key_for_concat_kdf
|
||||
from .oct_key import OctKey
|
||||
from .rsa_key import RSAKey
|
||||
from .ec_key import ECKey
|
||||
from .._rfc7516.models import (
|
||||
JWEAlgModel,
|
||||
JWEDirectEncryption,
|
||||
JWEKeyEncryption,
|
||||
JWEKeyWrapping,
|
||||
JWEKeyAgreement,
|
||||
JWEEncModel,
|
||||
Recipient,
|
||||
)
|
||||
from ..util import to_bytes, urlsafe_b64encode, urlsafe_b64decode
|
||||
from ..registry import HeaderParameter
|
||||
from ..errors import (
|
||||
InvalidKeyLengthError,
|
||||
DecodeError,
|
||||
SecurityWarning,
|
||||
)
|
||||
|
||||
|
||||
class DirectAlgEncryption(JWEDirectEncryption):
|
||||
name = "dir"
|
||||
description = "Direct use of a shared symmetric key"
|
||||
recommended = True
|
||||
|
||||
def compute_cek(self, size: int, recipient: Recipient[OctKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
cek = key.raw_value
|
||||
if len(cek) * 8 != size:
|
||||
raise InvalidKeyLengthError(f"A key of size {size} bits MUST be used")
|
||||
return cek
|
||||
|
||||
|
||||
class RSAAlgKeyEncryption(JWEKeyEncryption):
|
||||
#: A key of size 2048 bits or larger MUST be used with these algorithms
|
||||
#: RSA1_5, RSA-OAEP, RSA-OAEP-256
|
||||
key_size = 2048
|
||||
key_types = ["RSA"]
|
||||
|
||||
def __init__(self, name: str, description: str, pad_fn: padding.AsymmetricPadding, recommended: bool = False):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.padding = pad_fn
|
||||
self.recommended = recommended
|
||||
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[RSAKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("encrypt")
|
||||
if op_key.key_size < self.key_size:
|
||||
raise InvalidKeyLengthError(f"A key of size {self.key_size} bits or larger MUST be used")
|
||||
return op_key.encrypt(cek, self.padding)
|
||||
|
||||
def decrypt_cek(self, recipient: Recipient[RSAKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("decrypt")
|
||||
try:
|
||||
assert recipient.encrypted_key is not None
|
||||
cek = op_key.decrypt(recipient.encrypted_key, self.padding)
|
||||
except ValueError as error:
|
||||
raise DecodeError(str(error))
|
||||
return cek
|
||||
|
||||
|
||||
class AESAlgKeyWrapping(JWEKeyWrapping):
|
||||
def __init__(self, key_size: int, recommended: bool = False):
|
||||
self.name = f"A{key_size}KW"
|
||||
self.description = f"AES Key Wrap using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
self.recommended = recommended
|
||||
|
||||
def wrap_cek(self, cek: bytes, key: bytes) -> bytes:
|
||||
self.check_op_key(key)
|
||||
return aes_key_wrap(key, cek)
|
||||
|
||||
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes:
|
||||
self.check_op_key(key)
|
||||
try:
|
||||
cek = aes_key_unwrap(key, ek)
|
||||
except InvalidUnwrap:
|
||||
raise DecodeError("Unwrap AES key failed")
|
||||
return cek
|
||||
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("wrapKey")
|
||||
return self.wrap_cek(cek, op_key)
|
||||
|
||||
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("unwrapKey")
|
||||
assert recipient.encrypted_key is not None
|
||||
return self.unwrap_cek(recipient.encrypted_key, op_key)
|
||||
|
||||
|
||||
class AESGCMAlgKeyWrapping(JWEKeyWrapping):
|
||||
more_header_registry = {
|
||||
"iv": HeaderParameter("Initialization vector", "str", True),
|
||||
"tag": HeaderParameter("Authentication tag", "str", True),
|
||||
}
|
||||
|
||||
def __init__(self, key_size: int):
|
||||
self.name = f"A{key_size}GCMKW"
|
||||
self.description = f"Key wrapping with AES GCM using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
|
||||
def wrap_cek(self, cek: bytes, key: bytes) -> bytes: # pragma: no cover
|
||||
raise RuntimeError(f"{self.name} can not be used together with Key Agreement")
|
||||
|
||||
def unwrap_cek(self, ek: bytes, key: bytes) -> bytes: # pragma: no cover
|
||||
raise RuntimeError(f"{self.name} can not be used together with Key Agreement")
|
||||
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("wrapKey")
|
||||
self.check_op_key(op_key)
|
||||
|
||||
#: https://tools.ietf.org/html/rfc7518#section-4.7.1.1
|
||||
#: The "iv" (initialization vector) Header Parameter value is the
|
||||
#: base64url-encoded representation of the 96-bit IV value
|
||||
iv_size = 96
|
||||
iv = secrets.token_bytes(iv_size // 8)
|
||||
|
||||
cipher = Cipher(AES(op_key), GCM(iv))
|
||||
enc = cipher.encryptor()
|
||||
|
||||
encrypted_key = enc.update(cek) + enc.finalize()
|
||||
recipient.add_header("iv", urlsafe_b64encode(iv).decode("ascii"))
|
||||
recipient.add_header("tag", urlsafe_b64encode(enc.tag).decode("ascii"))
|
||||
return encrypted_key
|
||||
|
||||
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
op_key = key.get_op_key("unwrapKey")
|
||||
self.check_op_key(op_key)
|
||||
|
||||
headers = recipient.headers()
|
||||
assert "iv" in headers
|
||||
assert "tag" in headers
|
||||
iv = urlsafe_b64decode(to_bytes(headers["iv"]))
|
||||
tag = urlsafe_b64decode(to_bytes(headers["tag"]))
|
||||
|
||||
cipher = Cipher(AES(op_key), GCM(iv, tag))
|
||||
d = cipher.decryptor()
|
||||
try:
|
||||
assert recipient.encrypted_key is not None
|
||||
cek = d.update(recipient.encrypted_key) + d.finalize()
|
||||
except InvalidTag as error:
|
||||
raise DecodeError(str(error))
|
||||
return cek
|
||||
|
||||
|
||||
class ECDHESAlgKeyAgreement(JWEKeyAgreement):
|
||||
key_types = ["EC", "OKP"]
|
||||
more_header_registry = {
|
||||
"epk": HeaderParameter("Ephemeral Public Key", "jwk", True),
|
||||
"apu": HeaderParameter("Agreement PartyUInfo", "str"),
|
||||
"apv": HeaderParameter("Agreement PartyVInfo", "str"),
|
||||
}
|
||||
|
||||
# https://tools.ietf.org/html/rfc7518#section-4.6
|
||||
def __init__(self, key_wrapping: JWEKeyWrapping | None = None):
|
||||
if key_wrapping is None:
|
||||
self.name = "ECDH-ES"
|
||||
self.description = "ECDH-ES in the Direct Key Agreement mode"
|
||||
self.key_size = None
|
||||
self.recommended = True
|
||||
else:
|
||||
self.name = f"ECDH-ES+{key_wrapping.name}"
|
||||
self.description = f"ECDH-ES using Concat KDF and CEK wrapped with {key_wrapping.name}"
|
||||
self.key_size = key_wrapping.key_size
|
||||
self.recommended = key_wrapping.recommended
|
||||
self.key_wrapping = key_wrapping
|
||||
|
||||
def encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
recipient_key = recipient.recipient_key
|
||||
assert recipient_key is not None
|
||||
|
||||
ephemeral_key = recipient.ephemeral_key
|
||||
assert ephemeral_key is not None
|
||||
|
||||
shared_key = ephemeral_key.exchange_derive_key(recipient_key)
|
||||
headers = recipient.headers()
|
||||
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size)
|
||||
|
||||
def decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
headers = recipient.headers()
|
||||
assert "epk" in headers
|
||||
|
||||
recipient_key = recipient.recipient_key
|
||||
assert recipient_key is not None
|
||||
|
||||
self.check_key_type(recipient_key)
|
||||
ephemeral_key = recipient_key.import_key(headers["epk"])
|
||||
shared_key = recipient_key.exchange_derive_key(ephemeral_key)
|
||||
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size)
|
||||
|
||||
|
||||
def validate_p2c(value: int) -> None:
|
||||
if not isinstance(value, int):
|
||||
raise ValueError("must be an int")
|
||||
|
||||
# A minimum iteration count of 1000 is RECOMMENDED.
|
||||
if value < 1000:
|
||||
warnings.warn("A minimum iteration count of 1000 is RECOMMENDED", SecurityWarning)
|
||||
|
||||
max_value = 300000
|
||||
if value > max_value:
|
||||
raise ValueError(f"must be less than {max_value}")
|
||||
|
||||
|
||||
class PBES2HSAlgKeyEncryption(JWEKeyEncryption):
|
||||
# https://www.rfc-editor.org/rfc/rfc7518#section-4.8
|
||||
key_size: int
|
||||
more_header_registry = {
|
||||
"p2s": HeaderParameter("PBES2 Salt Input", "str", True),
|
||||
"p2c": HeaderParameter("PBES2 Count", validate_p2c, True),
|
||||
}
|
||||
key_types = ["oct"]
|
||||
|
||||
DEFAULT_P2C = 2048
|
||||
|
||||
def __init__(self, hash_size: int, key_wrapping: JWEKeyWrapping):
|
||||
self.name = f"PBES2-HS{hash_size}+{key_wrapping.name}"
|
||||
self.description = f"PBES2 with HMAC SHA-{hash_size} and {key_wrapping.name} wrapping"
|
||||
self.key_size = key_wrapping.key_size
|
||||
self.key_wrapping = key_wrapping
|
||||
self.hash_alg = getattr(hashes, f"SHA{hash_size}")()
|
||||
|
||||
def compute_derived_key(self, key: bytes, p2s: bytes, p2c: int) -> bytes:
|
||||
# The salt value used is (UTF8(Alg) || 0x00 || Salt Input)
|
||||
salt = to_bytes(self.name) + b"\x00" + p2s
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=self.hash_alg,
|
||||
length=self.key_size // 8,
|
||||
salt=salt,
|
||||
iterations=p2c,
|
||||
)
|
||||
return kdf.derive(key)
|
||||
|
||||
def encrypt_cek(self, cek: bytes, recipient: Recipient[OctKey]) -> bytes:
|
||||
headers = recipient.headers()
|
||||
if "p2s" not in headers:
|
||||
p2s = secrets.token_bytes(16)
|
||||
recipient.add_header("p2s", urlsafe_b64encode(p2s).decode("ascii"))
|
||||
else:
|
||||
p2s = urlsafe_b64decode(to_bytes(headers["p2s"]))
|
||||
|
||||
if "p2c" not in headers:
|
||||
# A minimum iteration count of 1000 is RECOMMENDED.
|
||||
p2c = self.DEFAULT_P2C
|
||||
recipient.add_header("p2c", p2c)
|
||||
else:
|
||||
p2c = headers["p2c"]
|
||||
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
self.check_key_type(key)
|
||||
kek = self.compute_derived_key(key.get_op_key("deriveKey"), p2s, p2c)
|
||||
return self.key_wrapping.wrap_cek(cek, kek)
|
||||
|
||||
def decrypt_cek(self, recipient: Recipient[OctKey]) -> bytes:
|
||||
headers = recipient.headers()
|
||||
assert "p2s" in headers
|
||||
assert "p2c" in headers
|
||||
p2s = urlsafe_b64decode(to_bytes(headers["p2s"]))
|
||||
p2c = headers["p2c"]
|
||||
|
||||
key = recipient.recipient_key
|
||||
assert key is not None
|
||||
|
||||
self.check_key_type(key)
|
||||
kek = self.compute_derived_key(key.get_op_key("deriveKey"), p2s, p2c)
|
||||
assert recipient.encrypted_key is not None
|
||||
return self.key_wrapping.unwrap_cek(recipient.encrypted_key, kek)
|
||||
|
||||
|
||||
RSA1_5 = RSAAlgKeyEncryption("RSA1_5", "RSAES-PKCS1-v1_5", padding.PKCS1v15())
|
||||
RSA1_5.security_warning = 'JWE algorithm "RSA1_5" is deprecated, via draft-ietf-jose-deprecate-none-rsa15-02'
|
||||
|
||||
A128KW = AESAlgKeyWrapping(128, True) # A128KW, Recommended
|
||||
A192KW = AESAlgKeyWrapping(192) # A192KW
|
||||
A256KW = AESAlgKeyWrapping(256, True) # A256KW, Recommended
|
||||
|
||||
|
||||
#: https://www.rfc-editor.org/rfc/rfc7518#section-4.1
|
||||
JWE_ALG_MODELS: list[JWEAlgModel] = [
|
||||
RSA1_5,
|
||||
RSAAlgKeyEncryption(
|
||||
"RSA-OAEP",
|
||||
"RSAES OAEP using default parameters",
|
||||
padding.OAEP(padding.MGF1(hashes.SHA1()), hashes.SHA1(), None),
|
||||
True,
|
||||
), # Recommended+
|
||||
RSAAlgKeyEncryption(
|
||||
"RSA-OAEP-256",
|
||||
"RSAES OAEP using SHA-256 and MGF1 with SHA-256",
|
||||
padding.OAEP(padding.MGF1(hashes.SHA256()), hashes.SHA256(), None),
|
||||
),
|
||||
A128KW,
|
||||
A192KW,
|
||||
A256KW,
|
||||
DirectAlgEncryption(), # dir, Recommended
|
||||
ECDHESAlgKeyAgreement(None), # ECDH-ES, Recommended+
|
||||
ECDHESAlgKeyAgreement(A128KW), # ECDH-ES+A128KW, Recommended
|
||||
ECDHESAlgKeyAgreement(A192KW), # ECDH-ES+A192KW
|
||||
ECDHESAlgKeyAgreement(A256KW), # ECDH-ES+A256KW, Recommended
|
||||
AESGCMAlgKeyWrapping(128), # A128GCMKW
|
||||
AESGCMAlgKeyWrapping(192), # A192GCMKW
|
||||
AESGCMAlgKeyWrapping(256), # A256GCMKW
|
||||
PBES2HSAlgKeyEncryption(256, A128KW), # PBES2-HS256+A128KW
|
||||
PBES2HSAlgKeyEncryption(384, A192KW), # PBES2-HS384+A192KW
|
||||
PBES2HSAlgKeyEncryption(512, A256KW), # PBES2-HS512+A256KW
|
||||
]
|
||||
|
||||
# compatible alias
|
||||
DirectAlgModel = DirectAlgEncryption
|
||||
AESAlgModel = AESAlgKeyWrapping
|
||||
ECDHESAlgModel = ECDHESAlgKeyAgreement
|
||||
AESGCMAlgModel = AESGCMAlgKeyWrapping
|
||||
PBES2HSAlgModel = PBES2HSAlgKeyEncryption
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
joserfc._rfc7518
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Cryptographic Models for Cryptographic Models for Content
|
||||
Encryption per `Section 5`_.
|
||||
|
||||
.. _`Section 5`: https://tools.ietf.org/html/rfc7518#section-5
|
||||
"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM, CBC
|
||||
from cryptography.hazmat.primitives.padding import PKCS7
|
||||
from cryptography.exceptions import InvalidTag
|
||||
from .._rfc7516.models import JWEEncModel
|
||||
from ..errors import DecodeError
|
||||
from .util import encode_int
|
||||
|
||||
|
||||
class CBCHS2EncModel(JWEEncModel):
|
||||
# The IV used is a 128-bit value generated randomly or
|
||||
# pseudo-randomly for use in the cipher.
|
||||
iv_size = 128
|
||||
recommended = True
|
||||
|
||||
def __init__(self, key_size: int, hash_type: int):
|
||||
self.name = f"A{key_size}CBC-HS{hash_type}"
|
||||
self.description = f"AES_{key_size}_CBC_HMAC_SHA_{hash_type} authenticated encryption algorithm"
|
||||
|
||||
# key size in bit
|
||||
self.key_size = key_size
|
||||
# key size in byte
|
||||
self.key_len = key_size // 8
|
||||
|
||||
self.cek_size = key_size * 2
|
||||
self.hash_alg = getattr(hashlib, f"sha{hash_type}")
|
||||
|
||||
def _hmac(self, ciphertext: bytes, aad: bytes, iv: bytes, key: bytes) -> bytes:
|
||||
al = encode_int(len(aad) * 8, 64)
|
||||
msg = aad + iv + ciphertext + al
|
||||
d = hmac.new(key, msg, self.hash_alg).digest()
|
||||
return d[: self.key_len]
|
||||
|
||||
def encrypt(self, plaintext: bytes, cek: bytes, iv: bytes, aad: bytes) -> tuple[bytes, bytes]:
|
||||
"""Key Encryption with AES_CBC_HMAC_SHA2."""
|
||||
hkey = cek[: self.key_len]
|
||||
ekey = cek[self.key_len :]
|
||||
|
||||
pad = PKCS7(AES.block_size).padder()
|
||||
padded_data = pad.update(plaintext) + pad.finalize()
|
||||
|
||||
cipher = Cipher(AES(ekey), CBC(iv))
|
||||
enc = cipher.encryptor()
|
||||
ciphertext = enc.update(padded_data) + enc.finalize()
|
||||
tag = self._hmac(ciphertext, aad, iv, hkey)
|
||||
return ciphertext, tag
|
||||
|
||||
def decrypt(self, ciphertext: bytes, tag: bytes, cek: bytes, iv: bytes, aad: bytes) -> bytes:
|
||||
"""Key Decryption with AES AES_CBC_HMAC_SHA2."""
|
||||
hkey = cek[: self.key_len]
|
||||
dkey = cek[self.key_len :]
|
||||
|
||||
ctag = self._hmac(ciphertext, aad, iv, hkey)
|
||||
if not hmac.compare_digest(ctag, tag):
|
||||
raise DecodeError("tag does not match")
|
||||
|
||||
cipher = Cipher(AES(dkey), CBC(iv))
|
||||
d = cipher.decryptor()
|
||||
data = d.update(ciphertext) + d.finalize()
|
||||
unpad = PKCS7(AES.block_size).unpadder()
|
||||
return unpad.update(data) + unpad.finalize()
|
||||
|
||||
|
||||
class GCMEncModel(JWEEncModel):
|
||||
# Use of an IV of size 96 bits is REQUIRED with this algorithm.
|
||||
# https://tools.ietf.org/html/rfc7518#section-5.3
|
||||
iv_size = 96
|
||||
recommended = True
|
||||
|
||||
def __init__(self, key_size: int):
|
||||
self.name = f"A{key_size}GCM"
|
||||
self.description = f"AES GCM using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
self.cek_size = key_size
|
||||
|
||||
def encrypt(self, plaintext: bytes, cek: bytes, iv: bytes, aad: bytes) -> tuple[bytes, bytes]:
|
||||
"""Key Encryption with AES GCM"""
|
||||
cipher = Cipher(AES(cek), GCM(iv))
|
||||
enc = cipher.encryptor()
|
||||
enc.authenticate_additional_data(aad)
|
||||
ciphertext = enc.update(plaintext) + enc.finalize()
|
||||
return ciphertext, enc.tag
|
||||
|
||||
def decrypt(self, ciphertext: bytes, tag: bytes, cek: bytes, iv: bytes, aad: bytes) -> bytes:
|
||||
"""Key Decryption with AES GCM"""
|
||||
cipher = Cipher(AES(cek), GCM(iv, tag))
|
||||
d = cipher.decryptor()
|
||||
d.authenticate_additional_data(aad)
|
||||
try:
|
||||
return d.update(ciphertext) + d.finalize()
|
||||
except InvalidTag as error:
|
||||
raise DecodeError(str(error))
|
||||
|
||||
|
||||
JWE_ENC_MODELS: list[JWEEncModel] = [
|
||||
CBCHS2EncModel(128, 256), # A128CBC-HS256
|
||||
CBCHS2EncModel(192, 384), # A192CBC-HS384
|
||||
CBCHS2EncModel(256, 512), # A256CBC-HS512
|
||||
GCMEncModel(128), # A128GCM
|
||||
GCMEncModel(192), # A192GCM
|
||||
GCMEncModel(256), # A256GCM
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
import zlib
|
||||
from .._rfc7516.models import JWEZipModel
|
||||
from ..errors import ExceededSizeError
|
||||
|
||||
|
||||
class DeflateZipModel(JWEZipModel):
|
||||
name = "DEF"
|
||||
description = "DEFLATE"
|
||||
|
||||
GZIP_HEAD = bytes([120, 156])
|
||||
MAX_SIZE = 250 * 1024
|
||||
|
||||
def compress(self, s: bytes) -> bytes:
|
||||
"""Compress bytes data with DEFLATE algorithm."""
|
||||
data = zlib.compress(s)
|
||||
# https://datatracker.ietf.org/doc/html/rfc1951
|
||||
# since DEF is always gzip, we can drop gzip headers and tail
|
||||
return data[2:-4]
|
||||
|
||||
def decompress(self, s: bytes) -> bytes:
|
||||
"""Decompress DEFLATE bytes data."""
|
||||
if s.startswith(self.GZIP_HEAD):
|
||||
decompressor = zlib.decompressobj()
|
||||
else:
|
||||
decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
value = decompressor.decompress(s, self.MAX_SIZE)
|
||||
if decompressor.unconsumed_tail:
|
||||
raise ExceededSizeError(f"Decompressed string exceeds {self.MAX_SIZE} bytes")
|
||||
return value
|
||||
|
||||
|
||||
JWE_ZIP_MODELS: list[JWEZipModel] = [DeflateZipModel()]
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
joserfc._rfc7518
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Originally designed in ``authlib.jose.rfc7518``.
|
||||
|
||||
"alg" (Algorithm) Header Parameter Values for JWS per `Section 3`_.
|
||||
|
||||
.. _`Section 3`: https://tools.ietf.org/html/rfc7518#section-3
|
||||
"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
import typing as t
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import (
|
||||
decode_dss_signature,
|
||||
encode_dss_signature,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from .._rfc7515.model import JWSAlgModel
|
||||
from ..errors import InvalidKeyCurveError
|
||||
from .oct_key import OctKey
|
||||
from .rsa_key import RSAKey
|
||||
from .ec_key import ECKey
|
||||
from .util import encode_int, decode_int
|
||||
|
||||
|
||||
class NoneAlgorithm(JWSAlgModel):
|
||||
name = "none"
|
||||
description = "No digital signature or MAC performed"
|
||||
security_warning = 'JWS algorithm "none" is deprecated, via draft-ietf-jose-deprecate-none-rsa15-02'
|
||||
|
||||
def sign(self, msg: bytes, key: t.Any) -> bytes:
|
||||
return b""
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: t.Any) -> bool:
|
||||
return sig == b""
|
||||
|
||||
|
||||
class HMACAlgorithm(JWSAlgModel):
|
||||
"""HMAC using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- HS256: HMAC using SHA-256
|
||||
- HS384: HMAC using SHA-384
|
||||
- HS512: HMAC using SHA-512
|
||||
"""
|
||||
|
||||
SHA256 = hashlib.sha256
|
||||
SHA384 = hashlib.sha384
|
||||
SHA512 = hashlib.sha512
|
||||
|
||||
def __init__(self, sha_type: t.Literal[256, 384, 512], recommended: bool = False):
|
||||
self.name = f"HS{sha_type}"
|
||||
self.description = f"HMAC using SHA-{sha_type}"
|
||||
self.recommended = recommended
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
self.algorithm_security = sha_type
|
||||
|
||||
def sign(self, msg: bytes, key: OctKey) -> bytes:
|
||||
# it is faster than the one in cryptography
|
||||
op_key = key.get_op_key("sign")
|
||||
if not op_key:
|
||||
# Defence-in-depth: OctKey.import_key rejects empty input, but
|
||||
# an OctKey can also be constructed directly through other
|
||||
# internal paths. An empty HMAC key produces a forgeable digest.
|
||||
raise ValueError("HMAC key must not be empty")
|
||||
return hmac.new(op_key, msg, self.hash_alg).digest()
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool:
|
||||
op_key = key.get_op_key("verify")
|
||||
if not op_key:
|
||||
raise ValueError("HMAC key must not be empty")
|
||||
v_sig = hmac.new(op_key, msg, self.hash_alg).digest()
|
||||
return hmac.compare_digest(sig, v_sig)
|
||||
|
||||
|
||||
class _RSAAlgModel(JWSAlgModel):
|
||||
key_type = "RSA"
|
||||
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
padding: padding.AsymmetricPadding
|
||||
|
||||
def __init__(self, sha_type: t.Literal[256, 384, 512], recommended: bool = False):
|
||||
self.recommended = recommended
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
self.algorithm_security = sha_type
|
||||
|
||||
def sign(self, msg: bytes, key: RSAKey) -> bytes:
|
||||
op_key = key.get_op_key("sign")
|
||||
return op_key.sign(msg, self.padding, self.hash_alg())
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: RSAKey) -> bool:
|
||||
op_key = key.get_op_key("verify")
|
||||
try:
|
||||
op_key.verify(sig, msg, self.padding, self.hash_alg())
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
class RSAAlgorithm(_RSAAlgModel):
|
||||
"""RSA using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- RS256: RSASSA-PKCS1-v1_5 using SHA-256
|
||||
- RS384: RSASSA-PKCS1-v1_5 using SHA-384
|
||||
- RS512: RSASSA-PKCS1-v1_5 using SHA-512
|
||||
"""
|
||||
|
||||
padding = padding.PKCS1v15()
|
||||
|
||||
def __init__(self, sha_type: t.Literal[256, 384, 512], recommended: bool = False):
|
||||
super().__init__(sha_type, recommended)
|
||||
self.name = f"RS{sha_type}"
|
||||
self.description = f"RSASSA-PKCS1-v1_5 using SHA-{sha_type}"
|
||||
|
||||
|
||||
class ESAlgorithm(JWSAlgModel):
|
||||
"""ECDSA using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- ES256: ECDSA using P-256 and SHA-256
|
||||
- ES384: ECDSA using P-384 and SHA-384
|
||||
- ES512: ECDSA using P-521 and SHA-512
|
||||
"""
|
||||
|
||||
key_type = "EC"
|
||||
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, name: str, curve: str, sha_type: t.Literal[256, 384, 512], recommended: bool = False):
|
||||
self.name = name
|
||||
self.curve = curve
|
||||
self.description = f"ECDSA using {self.curve} and SHA-{sha_type}"
|
||||
self.recommended = recommended
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
self.algorithm_security = sha_type
|
||||
|
||||
def check_key(self, key: ECKey) -> None:
|
||||
super().check_key(key)
|
||||
if key.curve_name != self.curve:
|
||||
raise InvalidKeyCurveError(f"Key for '{self.name}' not supported, only '{self.curve}' allowed")
|
||||
|
||||
def sign(self, msg: bytes, key: ECKey) -> bytes:
|
||||
op_key = key.get_op_key("sign")
|
||||
der_sig = op_key.sign(msg, ECDSA(self.hash_alg()))
|
||||
r, s = decode_dss_signature(der_sig)
|
||||
size = key.curve_key_size
|
||||
return encode_int(r, size) + encode_int(s, size)
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: ECKey) -> bool:
|
||||
key_size = key.curve_key_size
|
||||
length = (key_size + 7) // 8
|
||||
|
||||
if len(sig) != 2 * length:
|
||||
return False
|
||||
|
||||
r = decode_int(sig[:length])
|
||||
s = decode_int(sig[length:])
|
||||
der_sig = encode_dss_signature(r, s)
|
||||
|
||||
try:
|
||||
op_key = key.get_op_key("verify")
|
||||
op_key.verify(der_sig, msg, ECDSA(self.hash_alg()))
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
class RSAPSSAlgorithm(_RSAAlgModel):
|
||||
"""RSASSA-PSS using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
|
||||
- PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
|
||||
- PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
|
||||
"""
|
||||
|
||||
def __init__(self, sha_type: t.Literal[256, 384, 512]):
|
||||
super().__init__(sha_type, False)
|
||||
self.name = f"PS{sha_type}"
|
||||
self.description = f"RSASSA-PSS using SHA-{sha_type} and MGF1 with SHA-{sha_type}"
|
||||
self.padding = padding.PSS(mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg.digest_size)
|
||||
|
||||
|
||||
JWS_ALGORITHMS: list[JWSAlgModel] = [
|
||||
NoneAlgorithm(), # none
|
||||
HMACAlgorithm(256, True), # HS256
|
||||
HMACAlgorithm(384), # HS384
|
||||
HMACAlgorithm(512), # HS512
|
||||
RSAAlgorithm(256, True), # RS256
|
||||
RSAAlgorithm(384), # RS384
|
||||
RSAAlgorithm(512), # RS512
|
||||
ESAlgorithm("ES256", "P-256", 256, True),
|
||||
ESAlgorithm("ES384", "P-384", 384),
|
||||
ESAlgorithm("ES512", "P-521", 512),
|
||||
RSAPSSAlgorithm(256), # PS256
|
||||
RSAPSSAlgorithm(384), # PS384
|
||||
RSAPSSAlgorithm(512), # PS512
|
||||
]
|
||||
|
||||
# compatible
|
||||
NoneAlgModel = NoneAlgorithm
|
||||
HMACAlgModel = HMACAlgorithm
|
||||
RSAAlgModel = RSAAlgorithm
|
||||
ECAlgModel = ESAlgorithm
|
||||
RSAPSSAlgModel = RSAPSSAlgorithm
|
||||
@@ -0,0 +1,107 @@
|
||||
import typing as t
|
||||
from typing import Any
|
||||
import secrets
|
||||
import warnings
|
||||
from ..errors import SecurityWarning
|
||||
from ..util import (
|
||||
to_bytes,
|
||||
urlsafe_b64decode,
|
||||
urlsafe_b64encode,
|
||||
)
|
||||
from ..registry import KeyParameter
|
||||
from .._rfc7517.models import SymmetricKey, NativeKeyBinding
|
||||
from .._rfc7517.types import KeyParameters, DictKey, AnyKey
|
||||
|
||||
|
||||
POSSIBLE_UNSAFE_KEYS = (
|
||||
b"-----BEGIN ",
|
||||
b"---- BEGIN ",
|
||||
b"ssh-rsa ",
|
||||
b"ssh-dss ",
|
||||
b"ssh-ed25519 ",
|
||||
b"ecdsa-sha2-",
|
||||
)
|
||||
|
||||
|
||||
class OctBinding(NativeKeyBinding):
|
||||
@classmethod
|
||||
def convert_raw_key_to_dict(cls, raw_key: bytes, private: bool) -> DictKey:
|
||||
k = urlsafe_b64encode(raw_key).decode("utf-8")
|
||||
return {"k": k}
|
||||
|
||||
@classmethod
|
||||
def import_from_dict(cls, value: DictKey) -> bytes:
|
||||
return urlsafe_b64decode(to_bytes(value["k"]))
|
||||
|
||||
@classmethod
|
||||
def import_from_bytes(cls, value: bytes, password: Any | None = None) -> bytes:
|
||||
# security check
|
||||
if value.startswith(POSSIBLE_UNSAFE_KEYS):
|
||||
warnings.warn("This key should not be used as an oct key", SecurityWarning)
|
||||
return value
|
||||
|
||||
|
||||
class OctKey(SymmetricKey):
|
||||
"""OctKey is a symmetric key, defined by RFC7518 Section 6.4."""
|
||||
|
||||
key_type = "oct"
|
||||
binding = OctBinding
|
||||
|
||||
#: https://www.rfc-editor.org/rfc/rfc7518#section-6.4
|
||||
value_registry = {"k": KeyParameter("Key Value", "str", True, True)}
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: Any,
|
||||
value: AnyKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: Any = None,
|
||||
) -> "OctKey":
|
||||
key: OctKey = super(OctKey, cls).import_key(value, parameters, password)
|
||||
if not key.raw_value:
|
||||
# An empty oct key material produces a deterministic HMAC digest
|
||||
# that any party can reproduce, allowing trivial signature forgery
|
||||
# in JWS HS256/HS384/HS512 verification. Reject outright rather
|
||||
# than emitting a SecurityWarning that callers commonly suppress.
|
||||
raise ValueError("oct key material must not be empty")
|
||||
if len(key.raw_value) < 14:
|
||||
# https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
|
||||
warnings.warn("Key size should be >= 112 bits", SecurityWarning)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls,
|
||||
key_size: int | None = 256,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> "OctKey":
|
||||
"""Generate a ``OctKey`` with the given bit size (not bytes).
|
||||
|
||||
:param key_size: size in bit
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: must be True
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
"""
|
||||
if not private:
|
||||
raise ValueError("oct key can not be generated as public")
|
||||
|
||||
if key_size is None:
|
||||
key_size = 256
|
||||
|
||||
if key_size % 8 != 0:
|
||||
raise ValueError("Invalid bit size for oct key")
|
||||
|
||||
if key_size < 112:
|
||||
# https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
|
||||
warnings.warn("Key size should be >= 112 bits", SecurityWarning)
|
||||
|
||||
raw_key = secrets.token_bytes(key_size // 8)
|
||||
key: OctKey = cls(raw_key, raw_key, parameters)
|
||||
if auto_kid:
|
||||
key.ensure_kid()
|
||||
return key
|
||||
|
||||
def as_dict(self, private: bool = False, **params: t.Any) -> DictKey:
|
||||
return super().as_dict(private=True, **params)
|
||||
@@ -0,0 +1,201 @@
|
||||
import warnings
|
||||
import typing as t
|
||||
from functools import cached_property
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
generate_private_key,
|
||||
RSAPublicKey,
|
||||
RSAPrivateKey,
|
||||
RSAPrivateNumbers,
|
||||
RSAPublicNumbers,
|
||||
rsa_recover_prime_factors,
|
||||
rsa_crt_dmp1,
|
||||
rsa_crt_dmq1,
|
||||
rsa_crt_iqmp,
|
||||
)
|
||||
from ..registry import KeyParameter
|
||||
from ..errors import SecurityWarning, KeyParameterError
|
||||
from .._rfc7517.models import AsymmetricKey
|
||||
from .._rfc7517.pem import CryptographyBinding
|
||||
from .._rfc7517.types import KeyParameters, AnyKey
|
||||
from ..util import int_to_base64, base64_to_int
|
||||
|
||||
|
||||
RSADictKey = t.TypedDict(
|
||||
"RSADictKey",
|
||||
{
|
||||
"n": str,
|
||||
"e": str,
|
||||
"d": str,
|
||||
"p": str,
|
||||
"q": str,
|
||||
"dp": str,
|
||||
"dq": str,
|
||||
"qi": str,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
|
||||
|
||||
class RSABinding(CryptographyBinding):
|
||||
key_type = "RSA"
|
||||
ssh_type = b"ssh-rsa"
|
||||
_cryptography_key_types = (RSAPrivateKey, RSAPublicKey)
|
||||
|
||||
@staticmethod
|
||||
def generate_private_key(size: int) -> RSAPrivateKey:
|
||||
return generate_private_key(public_exponent=65537, key_size=size)
|
||||
|
||||
@classmethod
|
||||
def import_private_key(cls, obj: RSADictKey) -> RSAPrivateKey:
|
||||
if "oth" in obj: # pragma: no cover
|
||||
# https://tools.ietf.org/html/rfc7518#section-6.3.2.7
|
||||
raise ValueError('"oth" is not supported yet')
|
||||
|
||||
public_numbers = RSAPublicNumbers(base64_to_int(obj["e"]), base64_to_int(obj["n"]))
|
||||
|
||||
if has_all_prime_factors(obj):
|
||||
numbers = RSAPrivateNumbers(
|
||||
d=base64_to_int(obj["d"]),
|
||||
p=base64_to_int(obj["p"]),
|
||||
q=base64_to_int(obj["q"]),
|
||||
dmp1=base64_to_int(obj["dp"]),
|
||||
dmq1=base64_to_int(obj["dq"]),
|
||||
iqmp=base64_to_int(obj["qi"]),
|
||||
public_numbers=public_numbers,
|
||||
)
|
||||
else:
|
||||
d = base64_to_int(obj["d"])
|
||||
p, q = rsa_recover_prime_factors(public_numbers.n, d, public_numbers.e)
|
||||
numbers = RSAPrivateNumbers(
|
||||
d=d,
|
||||
p=p,
|
||||
q=q,
|
||||
dmp1=rsa_crt_dmp1(d, p),
|
||||
dmq1=rsa_crt_dmq1(d, q),
|
||||
iqmp=rsa_crt_iqmp(p, q),
|
||||
public_numbers=public_numbers,
|
||||
)
|
||||
|
||||
return numbers.private_key()
|
||||
|
||||
@classmethod
|
||||
def export_private_key(cls, key: RSAPrivateKey) -> RSADictKey:
|
||||
numbers = key.private_numbers()
|
||||
return {
|
||||
"n": int_to_base64(numbers.public_numbers.n),
|
||||
"e": int_to_base64(numbers.public_numbers.e),
|
||||
"d": int_to_base64(numbers.d),
|
||||
"p": int_to_base64(numbers.p),
|
||||
"q": int_to_base64(numbers.q),
|
||||
"dp": int_to_base64(numbers.dmp1),
|
||||
"dq": int_to_base64(numbers.dmq1),
|
||||
"qi": int_to_base64(numbers.iqmp),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def import_public_key(cls, obj: RSADictKey) -> RSAPublicKey:
|
||||
numbers = RSAPublicNumbers(base64_to_int(obj["e"]), base64_to_int(obj["n"]))
|
||||
return numbers.public_key()
|
||||
|
||||
@classmethod
|
||||
def export_public_key(cls, key: RSAPublicKey) -> dict[str, str]:
|
||||
numbers = key.public_numbers()
|
||||
return {"n": int_to_base64(numbers.n), "e": int_to_base64(numbers.e)}
|
||||
|
||||
|
||||
class RSAKey(AsymmetricKey[RSAPrivateKey, RSAPublicKey]):
|
||||
key_type = "RSA"
|
||||
#: Registry definition for RSA Key
|
||||
#: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
|
||||
value_registry = {
|
||||
"n": KeyParameter("Modulus", "str", private=False, required=True),
|
||||
"e": KeyParameter("Exponent", "str", private=False, required=True),
|
||||
"d": KeyParameter("Private Exponent", "str", private=True, required=False),
|
||||
"p": KeyParameter("First Prime Factor", "str", private=True, required=False),
|
||||
"q": KeyParameter("Second Prime Factor", "str", private=True, required=False),
|
||||
"dp": KeyParameter("First Factor CRT Exponent", "str", private=True, required=False),
|
||||
"dq": KeyParameter("Second Factor CRT Exponent", "str", private=True, required=False),
|
||||
"qi": KeyParameter("First CRT Coefficient", "str", private=True, required=False),
|
||||
"oth": KeyParameter("Other Primes Info", "none", private=True, required=False),
|
||||
}
|
||||
binding = RSABinding
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
return isinstance(self.raw_value, RSAPrivateKey)
|
||||
|
||||
@cached_property
|
||||
def public_key(self) -> RSAPublicKey:
|
||||
if isinstance(self.raw_value, RSAPrivateKey):
|
||||
return self.raw_value.public_key()
|
||||
return self.raw_value
|
||||
|
||||
@property
|
||||
def private_key(self) -> RSAPrivateKey | None:
|
||||
if isinstance(self.raw_value, RSAPrivateKey):
|
||||
return self.raw_value
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: t.Any,
|
||||
value: AnyKey | RSAPrivateKey | RSAPublicKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: t.Any = None,
|
||||
) -> "RSAKey":
|
||||
key: RSAKey
|
||||
if isinstance(value, (RSAPrivateKey, RSAPublicKey)):
|
||||
key = cls(value, value, parameters)
|
||||
else:
|
||||
key = super(RSAKey, cls).import_key(value, parameters, password)
|
||||
if key.raw_value.key_size < 2048:
|
||||
# https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
|
||||
warnings.warn("Key size should be >= 2048 bits", SecurityWarning)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls: type["RSAKey"],
|
||||
key_size: int | None = 2048,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> "RSAKey":
|
||||
"""Generate a ``RSAKey`` with the given bit size (not bytes).
|
||||
|
||||
:param key_size: size in bit
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: generate a private key or public key
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
"""
|
||||
if key_size is None:
|
||||
key_size = 2048
|
||||
|
||||
if key_size % 8 != 0:
|
||||
raise ValueError("A bit size must be a multiple of 8")
|
||||
|
||||
if key_size < 2048:
|
||||
# https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
|
||||
warnings.warn("Key size should be >= 2048 bits", SecurityWarning)
|
||||
|
||||
raw_key = cls.binding.generate_private_key(key_size)
|
||||
if private:
|
||||
key = cls(raw_key, raw_key, parameters)
|
||||
else:
|
||||
pub_key = raw_key.public_key()
|
||||
key = cls(pub_key, pub_key, parameters)
|
||||
if auto_kid:
|
||||
key.ensure_kid()
|
||||
return key
|
||||
|
||||
|
||||
def has_all_prime_factors(obj: RSADictKey) -> bool:
|
||||
props = ["p", "q", "dp", "dq", "qi"]
|
||||
props_found = [prop in obj for prop in props]
|
||||
if all(props_found):
|
||||
return True
|
||||
|
||||
if any(props_found):
|
||||
raise KeyParameterError("RSA key must include all parameters if any are present besides d")
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,12 @@
|
||||
import binascii
|
||||
|
||||
|
||||
def encode_int(num: int, bits: int) -> bytes:
|
||||
length = ((bits + 7) // 8) * 2
|
||||
padded_hex = "%0*x" % (length, num)
|
||||
big_endian = binascii.a2b_hex(padded_hex.encode("ascii"))
|
||||
return big_endian
|
||||
|
||||
|
||||
def decode_int(s: bytes) -> int:
|
||||
return int(binascii.b2a_hex(s), 16)
|
||||
@@ -0,0 +1,218 @@
|
||||
import time
|
||||
import json
|
||||
import datetime
|
||||
import calendar
|
||||
from json import JSONEncoder
|
||||
from typing import TypedDict, Any, Callable
|
||||
from ..util import to_bytes
|
||||
from ..errors import (
|
||||
MissingClaimError,
|
||||
InvalidClaimError,
|
||||
ExpiredTokenError,
|
||||
)
|
||||
|
||||
Claims = dict[str, Any]
|
||||
|
||||
|
||||
def convert_claims(claims: Claims, encoder_cls: type[JSONEncoder] | None = None) -> bytes:
|
||||
"""Turn claims into bytes payload."""
|
||||
for k in ["exp", "iat", "nbf"]:
|
||||
claim = claims.get(k)
|
||||
if isinstance(claim, datetime.datetime):
|
||||
claims[k] = calendar.timegm(claim.utctimetuple())
|
||||
|
||||
content = json.dumps(claims, ensure_ascii=False, separators=(",", ":"), cls=encoder_cls)
|
||||
return to_bytes(content)
|
||||
|
||||
|
||||
#: http://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||
class ClaimsOption(TypedDict, total=False):
|
||||
essential: bool
|
||||
allow_blank: bool | None
|
||||
value: str | int | bool
|
||||
values: list[str | int | bool] | list[str] | list[int] | list[bool]
|
||||
|
||||
|
||||
class BaseClaimsRegistry:
|
||||
"""Requesting "claims" for JWT with the given conditions."""
|
||||
|
||||
def __init__(self, **kwargs: ClaimsOption):
|
||||
self.options = kwargs
|
||||
|
||||
@property
|
||||
def essential_keys(self) -> set[str]:
|
||||
"""Returns the essential claim names."""
|
||||
return {key for key in self.options if self.options[key].get("essential")}
|
||||
|
||||
def check_value(self, claim_name: str, value: Any) -> None:
|
||||
"""
|
||||
Validates a given claim value based on predefined options.
|
||||
|
||||
:param claim_name: The name of the claim to validate.
|
||||
:param value: The value of the claim to be validated.
|
||||
:raises InvalidClaimError: If the value does not meet the claim's validation requirements.
|
||||
"""
|
||||
option = self.options.get(claim_name)
|
||||
if not option:
|
||||
return
|
||||
|
||||
allow_blank = option.get("allow_blank")
|
||||
if not allow_blank and value in (None, "", [], {}):
|
||||
raise InvalidClaimError(claim_name)
|
||||
|
||||
option_values = option.get("values")
|
||||
|
||||
if option_values is None:
|
||||
option_value = option.get("value")
|
||||
if option_value is not None:
|
||||
option_values = [option_value]
|
||||
|
||||
if not option_values:
|
||||
return
|
||||
|
||||
if isinstance(value, list):
|
||||
if not any(v in value for v in option_values):
|
||||
raise InvalidClaimError(claim_name)
|
||||
else:
|
||||
if value not in option_values:
|
||||
raise InvalidClaimError(claim_name)
|
||||
|
||||
def validate(self, claims: dict[str, Any]) -> None:
|
||||
"""
|
||||
Validates the provided claims against specified requirements and checks.
|
||||
|
||||
:param claims: A dictionary containing claims to validate.
|
||||
:raises InvalidClaimError: Raised if any claim fails validation.
|
||||
:raises MissingClaimError: Raised if one or more essential keys are missing.
|
||||
"""
|
||||
missed_keys = {key for key in self.essential_keys if claims.get(key) is None}
|
||||
if missed_keys:
|
||||
raise MissingClaimError(",".join(sorted(missed_keys)))
|
||||
|
||||
for key in claims:
|
||||
value = claims[key]
|
||||
func = getattr(self, "validate_" + key, None)
|
||||
if func:
|
||||
func(value)
|
||||
elif key in self.options:
|
||||
self.check_value(key, value)
|
||||
|
||||
|
||||
class JWTClaimsRegistry(BaseClaimsRegistry):
|
||||
"""A claims registry for validating JWT claims.
|
||||
|
||||
:param now: timestamp of "now" time
|
||||
:param leeway: leeway time in seconds
|
||||
:param kwargs: claims options
|
||||
"""
|
||||
|
||||
def __init__(self, now: int | Callable[[], int] | None = None, leeway: int = 0, **kwargs: ClaimsOption) -> None:
|
||||
if now is None:
|
||||
now = _generate_now
|
||||
self._now = now
|
||||
self.leeway = leeway
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@property
|
||||
def now(self) -> int:
|
||||
"""Returns the current timestamp."""
|
||||
if callable(self._now):
|
||||
return self._now()
|
||||
return self._now
|
||||
|
||||
def validate_iss(self, value: str) -> None:
|
||||
"""The "iss" (issuer) claim identifies the principal that issued the
|
||||
JWT. The processing of this claim is generally application specific.
|
||||
The "iss" value is a case-sensitive string containing a StringOrURI
|
||||
value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
raise InvalidClaimError("str", "Claim 'str' must be a StringOrURI value")
|
||||
self.check_value("iss", value)
|
||||
|
||||
def validate_sub(self, value: str) -> None:
|
||||
"""The "sub" (subject) claim identifies the principal that is the
|
||||
subject of the JWT. The claims in a JWT are normally statements
|
||||
about the subject. The subject value MUST either be scoped to be
|
||||
locally unique in the context of the issuer or be globally unique.
|
||||
The processing of this claim is generally application specific. The
|
||||
"sub" value is a case-sensitive string containing a StringOrURI
|
||||
value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
raise InvalidClaimError("sub", "Claim 'sub' must be a StringOrURI value")
|
||||
self.check_value("sub", value)
|
||||
|
||||
def validate_aud(self, value: str | list[str]) -> None:
|
||||
"""The "aud" (audience) claim identifies the recipients that the JWT is
|
||||
intended for. Each principal intended to process the JWT MUST
|
||||
identify itself with a value in the audience claim. If the principal
|
||||
processing the claim does not identify itself with a value in the
|
||||
"aud" claim when this claim is present, then the JWT MUST be
|
||||
rejected. In the general case, the "aud" value is an array of case-
|
||||
sensitive strings, each containing a StringOrURI value. In the
|
||||
special case when the JWT has one audience, the "aud" value MAY be a
|
||||
single case-sensitive string containing a StringOrURI value. The
|
||||
interpretation of audience values is generally application specific.
|
||||
Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if isinstance(value, str) or _validate_list_of_strings(value):
|
||||
self.check_value("aud", value)
|
||||
else:
|
||||
raise InvalidClaimError(
|
||||
"aud", "Claim 'aud' must be an array of StringOrURI value or a single StringOrURI value"
|
||||
)
|
||||
|
||||
def validate_exp(self, value: int) -> None:
|
||||
"""The "exp" (expiration time) claim identifies the expiration time on
|
||||
or after which the JWT MUST NOT be accepted for processing. The
|
||||
processing of the "exp" claim requires that the current date/time
|
||||
MUST be before the expiration date/time listed in the "exp" claim.
|
||||
Implementers MAY provide for some small leeway, usually no more than
|
||||
a few minutes, to account for clock skew. Its value MUST be a number
|
||||
containing a NumericDate value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if not _validate_numeric_time(value):
|
||||
raise InvalidClaimError("exp", "Claim 'exp' must be a NumericDate value")
|
||||
if value < (self.now - self.leeway):
|
||||
raise ExpiredTokenError("exp")
|
||||
self.check_value("exp", value)
|
||||
|
||||
def validate_nbf(self, value: int) -> None:
|
||||
"""The "nbf" (not before) claim identifies the time before which the JWT
|
||||
MUST NOT be accepted for processing. The processing of the "nbf"
|
||||
claim requires that the current date/time MUST be after or equal to
|
||||
the not-before date/time listed in the "nbf" claim. Implementers MAY
|
||||
provide for some small leeway, usually no more than a few minutes, to
|
||||
account for clock skew. Its value MUST be a number containing a
|
||||
NumericDate value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if not _validate_numeric_time(value):
|
||||
raise InvalidClaimError("nbf", "Claim 'nbf' must be a NumericDate value")
|
||||
if value > (self.now + self.leeway):
|
||||
raise InvalidClaimError("nbf", "The token is not yet valid")
|
||||
self.check_value("nbf", value)
|
||||
|
||||
def validate_iat(self, value: int) -> None:
|
||||
"""The "iat" (issued at) claim identifies the time at which the JWT was
|
||||
issued. This claim can be used to determine the age of the JWT. Its
|
||||
value MUST be a number containing a NumericDate value. Use of this
|
||||
claim is OPTIONAL.
|
||||
"""
|
||||
if not _validate_numeric_time(value):
|
||||
raise InvalidClaimError("iat", "Claim 'iat' must be a NumericDate value")
|
||||
if value > (self.now + self.leeway):
|
||||
raise InvalidClaimError("iat", "The token was issued in the future")
|
||||
self.check_value("iat", value)
|
||||
|
||||
|
||||
def _validate_numeric_time(s: int) -> bool:
|
||||
return isinstance(s, (int, float))
|
||||
|
||||
|
||||
def _validate_list_of_strings(s: list[str]) -> bool:
|
||||
return isinstance(s, list) and all(isinstance(v, str) for v in s)
|
||||
|
||||
|
||||
def _generate_now() -> int:
|
||||
return int(time.time())
|
||||
@@ -0,0 +1,38 @@
|
||||
import re
|
||||
from typing import Any
|
||||
from ..errors import InsecureClaimError
|
||||
|
||||
|
||||
SENSITIVE_NAMES = ("password", "token", "secret", "secret_key", "api_key")
|
||||
SENSITIVE_VALUES = re.compile(
|
||||
r"|".join(
|
||||
[
|
||||
# http://www.richardsramblings.com/regex/credit-card-numbers/
|
||||
r"\b(?:3[47]\d|(?:4\d|5[1-5]|65)\d{2}|6011)\d{12}\b",
|
||||
# various private keys
|
||||
r"-----BEGIN[A-Z ]+PRIVATE KEY-----.+-----END[A-Z ]+PRIVATE KEY-----",
|
||||
# social security numbers (US)
|
||||
r"^\b(?!(000|666|9))\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b",
|
||||
]
|
||||
),
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def check_sensitive_data(claims: dict[str, Any]) -> None:
|
||||
"""
|
||||
Checks for sensitive data within a dictionary of claims and raises an
|
||||
error if any sensitive names or values are detected.
|
||||
|
||||
:param claims: JWT claims to check for sensitive data
|
||||
:raises InsecureClaimError: if any sensitive names or values are detected
|
||||
"""
|
||||
for k in claims:
|
||||
# check claims key name
|
||||
if k in SENSITIVE_NAMES:
|
||||
raise InsecureClaimError(k)
|
||||
|
||||
# check claims values
|
||||
v = claims[k]
|
||||
if isinstance(v, str) and SENSITIVE_VALUES.search(v):
|
||||
raise InsecureClaimError(k)
|
||||
@@ -0,0 +1,31 @@
|
||||
import typing as t
|
||||
import json
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
from ..util import to_bytes, urlsafe_b64encode
|
||||
|
||||
|
||||
def calculate_thumbprint(
|
||||
value: dict[str, t.Any],
|
||||
digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256",
|
||||
) -> str:
|
||||
"""Calculate the thumbprint value of a Key, per RFC 7638.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from joserfc import jwk
|
||||
|
||||
jwk.thumbprint({
|
||||
'kty': 'oct',
|
||||
'k': 'sTBpI_oCHSyW-n0exSwhzNHwU9FGRioPauxWA84bnRU',
|
||||
})
|
||||
# 'DCdRGGDKvhAJgmVlCp6tosc2T9ELtd30S_15vn8bhrI'
|
||||
"""
|
||||
sorted_fields = sorted(value.keys())
|
||||
data = OrderedDict()
|
||||
for k in sorted_fields:
|
||||
data[k] = value[k]
|
||||
json_data = json.dumps(data, ensure_ascii=True, separators=(",", ":"))
|
||||
hash_value = hashlib.new(digest_method, to_bytes(json_data))
|
||||
digest_data = hash_value.digest()
|
||||
return urlsafe_b64encode(digest_data).decode("utf-8")
|
||||
@@ -0,0 +1,90 @@
|
||||
import re
|
||||
from typing import Any
|
||||
from ..util import (
|
||||
to_bytes,
|
||||
to_str,
|
||||
json_b64encode,
|
||||
urlsafe_b64encode,
|
||||
urlsafe_b64decode,
|
||||
)
|
||||
from .._rfc7515.model import JWSAlgModel, CompactSignature
|
||||
from .._rfc7515.compact import decode_header
|
||||
from .._rfc7515.registry import JWSRegistry, default_registry
|
||||
from ..errors import DecodeError
|
||||
from .util import is_rfc7797_enabled
|
||||
|
||||
|
||||
def sign_rfc7515_compact(obj: CompactSignature, alg: JWSAlgModel, key: Any) -> bytes:
|
||||
header_segment = json_b64encode(obj.headers())
|
||||
signing_input = header_segment + b"." + obj.payload
|
||||
signature = urlsafe_b64encode(alg.sign(signing_input, key))
|
||||
|
||||
# if need to detach payload
|
||||
if __is_urlsafe_characters(obj.payload):
|
||||
out = signing_input + b"." + signature
|
||||
else:
|
||||
out = header_segment + b".." + signature
|
||||
return out
|
||||
|
||||
|
||||
def extract_rfc7515_compact(
|
||||
value: bytes, payload: bytes | str | None = None, registry: JWSRegistry | None = None
|
||||
) -> CompactSignature:
|
||||
"""Extract the JWS Compact Serialization from bytes to object.
|
||||
|
||||
:param value: JWS in bytes
|
||||
:param payload: optional payload, required with detached content
|
||||
:param registry: optional JWSRegistry instance
|
||||
:raise DecodeError: when decoding fails
|
||||
"""
|
||||
parts = value.split(b".")
|
||||
if len(parts) != 3:
|
||||
raise DecodeError("Invalid JSON Web Signature")
|
||||
|
||||
if registry is None:
|
||||
registry = default_registry
|
||||
|
||||
header_segment, payload_segment, signature_segment = parts
|
||||
|
||||
registry.validate_header_size(header_segment)
|
||||
registry.validate_signature_size(signature_segment)
|
||||
if payload_segment:
|
||||
registry.validate_payload_size(payload_segment)
|
||||
|
||||
protected = decode_header(header_segment)
|
||||
|
||||
if is_rfc7797_enabled(protected):
|
||||
if not payload_segment and payload:
|
||||
payload_segment = to_bytes(payload)
|
||||
registry.validate_payload_size(payload_segment)
|
||||
payload = payload_segment
|
||||
else:
|
||||
if not payload_segment and payload:
|
||||
payload = to_bytes(payload)
|
||||
payload_segment = urlsafe_b64encode(payload)
|
||||
else:
|
||||
try:
|
||||
payload = urlsafe_b64decode(payload_segment)
|
||||
except (TypeError, ValueError):
|
||||
raise DecodeError("Invalid payload")
|
||||
|
||||
obj = CompactSignature(protected, payload)
|
||||
obj.segments.update(
|
||||
{
|
||||
"header": header_segment,
|
||||
"payload": payload_segment,
|
||||
"signature": signature_segment,
|
||||
}
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc7797#section-5.2
|
||||
# the application MUST ensure that the payload contains only the URL-safe
|
||||
# characters 'a'-'z', 'A'-'Z', '0'-'9', dash ('-'), underscore ('_'),
|
||||
# and tilde ('~')
|
||||
_re_urlsafe = re.compile("^[a-zA-Z0-9-_~]+$")
|
||||
|
||||
|
||||
def __is_urlsafe_characters(s: bytes | str) -> bool:
|
||||
return bool(_re_urlsafe.match(to_str(s)))
|
||||
@@ -0,0 +1,51 @@
|
||||
from .._rfc7515.types import FlattenedJSONSerialization, JSONSignatureDict
|
||||
from .._rfc7515.model import HeaderMember, FlattenedJSONSignature
|
||||
from .._rfc7515.registry import JWSRegistry
|
||||
from .._rfc7515.json import sign_json_member, FindKey
|
||||
from ..util import to_bytes, json_b64decode, urlsafe_b64decode
|
||||
from ..errors import DecodeError
|
||||
from .util import is_rfc7797_enabled
|
||||
|
||||
|
||||
def sign_rfc7797_json(
|
||||
member: HeaderMember,
|
||||
payload: bytes,
|
||||
registry: JWSRegistry,
|
||||
find_key: FindKey,
|
||||
) -> FlattenedJSONSerialization:
|
||||
signature = sign_json_member(payload, member, registry, find_key)
|
||||
data: FlattenedJSONSerialization = {"payload": payload.decode("utf-8"), **signature}
|
||||
return data
|
||||
|
||||
|
||||
def extract_rfc7797_json(value: FlattenedJSONSerialization, registry: JWSRegistry) -> FlattenedJSONSignature:
|
||||
if "protected" in value:
|
||||
protected_segment = to_bytes(value["protected"])
|
||||
registry.validate_header_size(protected_segment)
|
||||
protected = json_b64decode(protected_segment)
|
||||
else:
|
||||
protected = None
|
||||
|
||||
header = value.get("header")
|
||||
member = HeaderMember(protected, header)
|
||||
|
||||
payload_segment: bytes = value["payload"].encode("utf-8")
|
||||
registry.validate_payload_size(payload_segment)
|
||||
|
||||
if is_rfc7797_enabled(member.headers()):
|
||||
payload = payload_segment
|
||||
else:
|
||||
try:
|
||||
payload = urlsafe_b64decode(payload_segment)
|
||||
except (TypeError, ValueError):
|
||||
raise DecodeError("Invalid payload")
|
||||
|
||||
obj = FlattenedJSONSignature(member, payload)
|
||||
_sig: JSONSignatureDict = {"signature": value["signature"]}
|
||||
if "protected" in value:
|
||||
_sig["protected"] = value["protected"]
|
||||
if "header" in value:
|
||||
_sig["header"] = value["header"]
|
||||
obj.signature = _sig
|
||||
obj.segments = {"payload": payload_segment}
|
||||
return obj
|
||||
@@ -0,0 +1,17 @@
|
||||
from ..registry import Header
|
||||
from ..errors import MissingCritHeaderError
|
||||
|
||||
|
||||
def is_rfc7797_enabled(header: Header) -> bool:
|
||||
if "b64" not in header:
|
||||
return False
|
||||
|
||||
if header["b64"] is True:
|
||||
return False
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc7797#section-6
|
||||
crit = header.get("crit")
|
||||
if isinstance(crit, list) and "b64" in crit:
|
||||
return True
|
||||
|
||||
raise MissingCritHeaderError("b64")
|
||||
@@ -0,0 +1,35 @@
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
|
||||
from ..errors import InvalidKeyTypeError
|
||||
from .._rfc7515.model import JWSAlgModel
|
||||
from .okp_key import OKPKey
|
||||
|
||||
|
||||
class EdDSAAlgorithm(JWSAlgModel):
|
||||
name = "EdDSA"
|
||||
description = "Edwards-curve Digital Signature Algorithm for JWS"
|
||||
key_type = "OKP"
|
||||
security_warning = "EdDSA is deprecated via RFC 9864"
|
||||
|
||||
def sign(self, msg: bytes, key: OKPKey) -> bytes:
|
||||
op_key = key.get_op_key("sign")
|
||||
if not isinstance(op_key, (Ed25519PrivateKey, Ed448PrivateKey)):
|
||||
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' or 'Ed448' OKP key")
|
||||
return op_key.sign(msg)
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: OKPKey) -> bool:
|
||||
op_key = key.get_op_key("verify")
|
||||
if not isinstance(op_key, (Ed25519PublicKey, Ed448PublicKey)):
|
||||
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' or 'Ed448' OKP key")
|
||||
try:
|
||||
op_key.verify(sig, msg)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
EdDSA = EdDSAAlgorithm()
|
||||
|
||||
# compatible
|
||||
EdDSAAlgModel = EdDSAAlgorithm
|
||||
@@ -0,0 +1,326 @@
|
||||
import typing as t
|
||||
from functools import cached_property
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey, X25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey, X448PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
PublicFormat,
|
||||
PrivateFormat,
|
||||
NoEncryption,
|
||||
)
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from .._rfc7517.models import CurveKey
|
||||
from .._rfc7517.types import KeyParameters, AnyKey
|
||||
from .._rfc7517.pem import CryptographyBinding
|
||||
from ..errors import InvalidExchangeKeyError, InvalidKeyCurveError
|
||||
from ..util import to_bytes, urlsafe_b64decode, urlsafe_b64encode
|
||||
from ..registry import KeyParameter
|
||||
|
||||
|
||||
LiteralCurves = t.Literal["Ed25519", "Ed448", "X25519", "X448"]
|
||||
PublicOKPKey = t.Union[Ed25519PublicKey, Ed448PublicKey, X25519PublicKey, X448PublicKey]
|
||||
PrivateOKPKey = t.Union[Ed25519PrivateKey, Ed448PrivateKey, X25519PrivateKey, X448PrivateKey]
|
||||
OKPDictKey = t.TypedDict(
|
||||
"OKPDictKey",
|
||||
{
|
||||
"crv": LiteralCurves,
|
||||
"x": str,
|
||||
"d": str,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
PUBLIC_KEYS_MAP: dict[str, type[PublicOKPKey]] = {
|
||||
"Ed25519": Ed25519PublicKey,
|
||||
"Ed448": Ed448PublicKey,
|
||||
"X25519": X25519PublicKey,
|
||||
"X448": X448PublicKey,
|
||||
}
|
||||
PRIVATE_KEYS_MAP: dict[str, type[PrivateOKPKey]] = {
|
||||
"Ed25519": Ed25519PrivateKey,
|
||||
"Ed448": Ed448PrivateKey,
|
||||
"X25519": X25519PrivateKey,
|
||||
"X448": X448PrivateKey,
|
||||
}
|
||||
OKP_SEED_SIZES: dict[LiteralCurves, int] = {
|
||||
"Ed25519": 32,
|
||||
"Ed448": 57,
|
||||
"X25519": 32,
|
||||
"X448": 56,
|
||||
}
|
||||
PrivateKeyTypes = (Ed25519PrivateKey, Ed448PrivateKey, X25519PrivateKey, X448PrivateKey)
|
||||
|
||||
|
||||
class OKPBinding(CryptographyBinding):
|
||||
key_type = "OKP"
|
||||
ssh_type = b"ssh-ed25519"
|
||||
_cryptography_key_types = (
|
||||
Ed25519PublicKey,
|
||||
Ed25519PrivateKey,
|
||||
Ed448PublicKey,
|
||||
Ed448PrivateKey,
|
||||
X25519PublicKey,
|
||||
X25519PrivateKey,
|
||||
X448PublicKey,
|
||||
X448PrivateKey,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_private_key(crv: LiteralCurves) -> PrivateOKPKey:
|
||||
if crv not in PRIVATE_KEYS_MAP:
|
||||
raise InvalidKeyCurveError(f"Invalid curve value: '{crv}'")
|
||||
crv_key: type[PrivateOKPKey] = PRIVATE_KEYS_MAP[crv]
|
||||
return crv_key.generate()
|
||||
|
||||
@staticmethod
|
||||
def from_private_bytes(crv: LiteralCurves, data: bytes) -> PrivateOKPKey:
|
||||
crv_key: type[PrivateOKPKey] = PRIVATE_KEYS_MAP[crv]
|
||||
return crv_key.from_private_bytes(data)
|
||||
|
||||
@staticmethod
|
||||
def from_public_bytes(crv: LiteralCurves, data: bytes) -> PublicOKPKey:
|
||||
crv_key: type[PublicOKPKey] = PUBLIC_KEYS_MAP[crv]
|
||||
return crv_key.from_public_bytes(data)
|
||||
|
||||
@classmethod
|
||||
def import_private_key(cls, obj: OKPDictKey) -> PrivateOKPKey:
|
||||
d = urlsafe_b64decode(to_bytes(obj["d"]))
|
||||
return cls.from_private_bytes(obj["crv"], d)
|
||||
|
||||
@classmethod
|
||||
def import_public_key(cls, obj: OKPDictKey) -> PublicOKPKey:
|
||||
x = urlsafe_b64decode(to_bytes(obj["x"]))
|
||||
return cls.from_public_bytes(obj["crv"], x)
|
||||
|
||||
@classmethod
|
||||
def export_private_key(cls, key: PrivateOKPKey) -> dict[str, str]:
|
||||
obj = cls.export_public_key(key.public_key())
|
||||
d_bytes = key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
|
||||
obj["d"] = urlsafe_b64encode(d_bytes).decode("utf-8")
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def export_public_key(cls, key: PublicOKPKey) -> dict[str, str]:
|
||||
x_bytes = key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
return {
|
||||
"crv": get_key_curve(key),
|
||||
"x": urlsafe_b64encode(x_bytes).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
class OKPKey(CurveKey[PrivateOKPKey, PublicOKPKey]):
|
||||
"""Key class of the ``OKP`` key type."""
|
||||
|
||||
key_type = "OKP"
|
||||
#: Registry definition for OKP Key
|
||||
#: https://www.rfc-editor.org/rfc/rfc8037#section-2
|
||||
value_registry = {
|
||||
"crv": KeyParameter("Curve", "str", private=False, required=True),
|
||||
"x": KeyParameter("X Coordinate", "str", private=False, required=True),
|
||||
"d": KeyParameter("OKP Private Key", "str", private=True, required=False),
|
||||
}
|
||||
binding = OKPBinding
|
||||
|
||||
def exchange_derive_key(self, key: "OKPKey") -> bytes:
|
||||
# used in ECDH-ES Algorithms
|
||||
pubkey = t.cast(t.Union[X25519PublicKey, X448PublicKey], key.get_op_key("deriveKey"))
|
||||
|
||||
# this if else logic is used for type hints
|
||||
if isinstance(self.private_key, X25519PrivateKey) and isinstance(pubkey, X25519PublicKey):
|
||||
return self.private_key.exchange(pubkey)
|
||||
elif isinstance(self.private_key, X448PrivateKey) and isinstance(pubkey, X448PublicKey):
|
||||
return self.private_key.exchange(pubkey)
|
||||
raise InvalidExchangeKeyError()
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
return isinstance(self.raw_value, PrivateKeyTypes)
|
||||
|
||||
@cached_property
|
||||
def public_key(self) -> PublicOKPKey:
|
||||
if isinstance(self.raw_value, PrivateKeyTypes):
|
||||
return self.raw_value.public_key()
|
||||
return self.raw_value
|
||||
|
||||
@property
|
||||
def private_key(self) -> PrivateOKPKey | None:
|
||||
if isinstance(self.raw_value, PrivateKeyTypes):
|
||||
return self.raw_value
|
||||
return None
|
||||
|
||||
@property
|
||||
def curve_name(self) -> LiteralCurves:
|
||||
return get_key_curve(self.raw_value)
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: t.Any,
|
||||
value: AnyKey | PrivateOKPKey | PublicOKPKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: t.Any = None,
|
||||
) -> "OKPKey":
|
||||
key: OKPKey
|
||||
if isinstance(
|
||||
value,
|
||||
(
|
||||
Ed25519PrivateKey,
|
||||
Ed448PrivateKey,
|
||||
X25519PrivateKey,
|
||||
X448PrivateKey,
|
||||
Ed25519PublicKey,
|
||||
Ed448PublicKey,
|
||||
X25519PublicKey,
|
||||
X448PublicKey,
|
||||
),
|
||||
):
|
||||
key = cls(value, value, parameters)
|
||||
else:
|
||||
key = super(OKPKey, cls).import_key(value, parameters, password)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls: type["OKPKey"],
|
||||
crv: LiteralCurves | None = "Ed25519",
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> "OKPKey":
|
||||
"""Generate a ``OKPKey`` with the given "crv" value.
|
||||
|
||||
:param crv: OKPKey curve name
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: generate a private key or public key
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
"""
|
||||
if crv is None:
|
||||
raw_key = cls.binding.generate_private_key("Ed25519")
|
||||
else:
|
||||
raw_key = cls.binding.generate_private_key(crv)
|
||||
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
|
||||
|
||||
@classmethod
|
||||
def derive_key(
|
||||
cls: type["OKPKey"],
|
||||
secret: bytes | str,
|
||||
crv: LiteralCurves = "Ed25519",
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
kdf_name: t.Literal["HKDF", "PBKDF2"] = "HKDF",
|
||||
kdf_options: dict[str, t.Any] | None = None,
|
||||
) -> "OKPKey":
|
||||
"""
|
||||
Derives a key from a given input secret using a specified key derivation function
|
||||
(KDF) and elliptic curve algorithm.
|
||||
|
||||
To derive a key using **HKDF**, the ``kdf_options`` may contain the ``algorithm``,
|
||||
``salt`` and ``info`` values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from joserfc.jwk import OKPKey
|
||||
|
||||
# default kdf_name is HKDF, algorithm is SHA256
|
||||
OKPKey.derive_key("secret")
|
||||
# equivalent to
|
||||
OKPKey.derive_key(
|
||||
"secret", "Ed25519",
|
||||
kdf_name="HKDF",
|
||||
kdf_options={
|
||||
"algorithm": hashes.SHA256(),
|
||||
"salt": b"joserfc:OKP:HKDF:Ed25519",
|
||||
"info": b"",
|
||||
}
|
||||
)
|
||||
|
||||
To derive a key using **PBKDF2**, the ``kdf_options`` may contain the ``algorithm``,
|
||||
``salt`` and ``iterations`` values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from joserfc.jwk import OKPKey
|
||||
|
||||
OKPKey.derive_key("secret", kdf_name="PBKDF2")
|
||||
# equivalent to
|
||||
OKPKey.derive_key(
|
||||
"secret", "Ed25519",
|
||||
kdf_name="PBKDF2",
|
||||
kdf_options={
|
||||
"algorithm": hashes.SHA256(),
|
||||
"salt": b"joserfc:OKP:PBKDF2:Ed25519",
|
||||
"iterations": 100000,
|
||||
}
|
||||
)
|
||||
|
||||
:param secret: The input secret used for key derivation
|
||||
:param crv: OKPKey curve name
|
||||
:param parameters: extra parameter in JWK
|
||||
:param private: generate a private key or public key
|
||||
:param auto_kid: add ``kid`` automatically
|
||||
:param kdf_name: Key derivation function name
|
||||
:param kdf_options: Additional options for the KDF
|
||||
"""
|
||||
if kdf_options is None:
|
||||
kdf_options = {}
|
||||
|
||||
algorithm = kdf_options.pop("algorithm", None)
|
||||
if algorithm is None:
|
||||
algorithm = hashes.SHA256()
|
||||
|
||||
kdf_options.setdefault("salt", to_bytes(f"joserfc:OKP:{kdf_name}:{crv}"))
|
||||
if kdf_name == "HKDF":
|
||||
kdf_options.setdefault("info", b"")
|
||||
hkdf = HKDF(
|
||||
algorithm=algorithm,
|
||||
length=OKP_SEED_SIZES[crv],
|
||||
**kdf_options,
|
||||
)
|
||||
seed = hkdf.derive(to_bytes(secret))
|
||||
elif kdf_name == "PBKDF2":
|
||||
kdf_options.setdefault("iterations", 100000)
|
||||
pbkdf2 = PBKDF2HMAC(
|
||||
algorithm=algorithm,
|
||||
length=OKP_SEED_SIZES[crv],
|
||||
**kdf_options,
|
||||
)
|
||||
seed = pbkdf2.derive(to_bytes(secret))
|
||||
else:
|
||||
raise ValueError(f"Invalid kdf value: '{kdf_name}'")
|
||||
|
||||
raw_key = cls.binding.from_private_bytes(crv, seed)
|
||||
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
|
||||
|
||||
|
||||
def get_key_curve(key: t.Union[PublicOKPKey, PrivateOKPKey]) -> LiteralCurves:
|
||||
if isinstance(key, (Ed25519PublicKey, Ed25519PrivateKey)):
|
||||
return "Ed25519"
|
||||
elif isinstance(key, (Ed448PublicKey, Ed448PrivateKey)):
|
||||
return "Ed448"
|
||||
elif isinstance(key, (X25519PublicKey, X25519PrivateKey)):
|
||||
return "X25519"
|
||||
elif isinstance(key, (X448PublicKey, X448PrivateKey)):
|
||||
return "X448"
|
||||
raise ValueError("Invalid key") # pragma: no cover
|
||||
|
||||
|
||||
def _wrap_key(
|
||||
cls: type[OKPKey],
|
||||
raw_key: PrivateOKPKey,
|
||||
private: bool,
|
||||
auto_kid: bool,
|
||||
parameters: KeyParameters | None = None,
|
||||
) -> OKPKey:
|
||||
if private:
|
||||
key = cls(raw_key, raw_key, parameters)
|
||||
else:
|
||||
pub_key = raw_key.public_key()
|
||||
key = cls(pub_key, pub_key, parameters)
|
||||
if auto_kid:
|
||||
key.ensure_kid()
|
||||
return key
|
||||
@@ -0,0 +1,13 @@
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP256K1
|
||||
from .._rfc7518.ec_key import ECKey
|
||||
from .._rfc7518.jws_algs import ESAlgorithm
|
||||
|
||||
ES256K = ESAlgorithm("ES256K", "secp256k1", 256)
|
||||
|
||||
|
||||
def register_secp256k1() -> None:
|
||||
# https://tools.ietf.org/html/rfc8812#section-3.1
|
||||
ECKey.binding.register_curve("secp256k1", SECP256K1)
|
||||
|
||||
|
||||
__all__ = ["ES256K", "register_secp256k1"]
|
||||
@@ -0,0 +1,29 @@
|
||||
import typing as t
|
||||
from .._rfc7638 import calculate_thumbprint
|
||||
|
||||
JWK_THUMBPRINT_URN = "urn:ietf:params:oauth:jwk-thumbprint"
|
||||
|
||||
|
||||
def calculate_thumbprint_uri(
|
||||
value: dict[str, t.Any],
|
||||
digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256",
|
||||
) -> str:
|
||||
"""Calculate JWK thumbprint URI, defined by RFC9278.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from joserfc import jwk
|
||||
|
||||
jwk.thumbprint({
|
||||
'kty': 'oct',
|
||||
'k': 'sTBpI_oCHSyW-n0exSwhzNHwU9FGRioPauxWA84bnRU',
|
||||
})
|
||||
# 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:DCdRGGDKvhAJgmVlCp6tosc2T9ELtd30S_15vn8bhrI'
|
||||
"""
|
||||
thumbprint = calculate_thumbprint(value, digest_method=digest_method)
|
||||
return concat_thumbprint_uri(thumbprint, digest_method=digest_method)
|
||||
|
||||
|
||||
def concat_thumbprint_uri(value: str, digest_method: t.Literal["sha256", "sha384", "sha512"]) -> str:
|
||||
method = digest_method.replace("sha", "sha-")
|
||||
return f"{JWK_THUMBPRINT_URN}:{method}:{value}"
|
||||
@@ -0,0 +1,8 @@
|
||||
from .jws_eddsa import JWS_ALGORITHMS, Ed25519, Ed448
|
||||
|
||||
|
||||
__all__ = [
|
||||
"JWS_ALGORITHMS",
|
||||
"Ed25519",
|
||||
"Ed448",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
import typing as t
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
|
||||
from ..errors import InvalidKeyCurveError
|
||||
from .._rfc7515.model import JWSAlgModel
|
||||
from .._rfc8037.okp_key import OKPKey
|
||||
|
||||
|
||||
class EdDSAAlgorithm(JWSAlgModel):
|
||||
key_type = "OKP"
|
||||
|
||||
def __init__(self, curve: t.Literal["Ed25519", "Ed448"]):
|
||||
self.name = curve
|
||||
self.curve = curve
|
||||
self.description = f"EdDSA using the {curve} parameter set"
|
||||
|
||||
def check_key(self, key: OKPKey) -> None:
|
||||
super().check_key(key)
|
||||
if key.curve_name != self.curve:
|
||||
raise InvalidKeyCurveError(f"Key for '{self.name}' not supported, only '{self.curve}' allowed")
|
||||
|
||||
def sign(self, msg: bytes, key: OKPKey) -> bytes:
|
||||
op_key = t.cast(t.Union[Ed25519PrivateKey, Ed448PrivateKey], key.get_op_key("sign"))
|
||||
return op_key.sign(msg)
|
||||
|
||||
def verify(self, msg: bytes, sig: bytes, key: OKPKey) -> bool:
|
||||
op_key = t.cast(t.Union[Ed25519PublicKey, Ed448PublicKey], key.get_op_key("verify"))
|
||||
try:
|
||||
op_key.verify(sig, msg)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
Ed25519 = EdDSAAlgorithm("Ed25519")
|
||||
Ed448 = EdDSAAlgorithm("Ed448")
|
||||
|
||||
JWS_ALGORITHMS: list[EdDSAAlgorithm] = [Ed25519, Ed448]
|
||||
@@ -0,0 +1,40 @@
|
||||
from Crypto.Cipher import ChaCha20_Poly1305
|
||||
from .._rfc7516.registry import JWERegistry
|
||||
from .._rfc7516.models import JWEEncModel
|
||||
|
||||
__all__ = ["ChaCha20EncModel", "JWE_ENC_MODELS", "register_chacha20_poly1305"]
|
||||
|
||||
|
||||
class ChaCha20EncModel(JWEEncModel):
|
||||
# https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4
|
||||
cek_size = 256
|
||||
recommended = False
|
||||
|
||||
def __init__(self, name: str, description: str, iv_size: int):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.iv_size = iv_size
|
||||
|
||||
def encrypt(self, plaintext: bytes, cek: bytes, iv: bytes, aad: bytes) -> tuple[bytes, bytes]:
|
||||
"""Key Encryption with AEAD_CHACHA20_POLY1305"""
|
||||
chacha = ChaCha20_Poly1305.new(key=cek, nonce=iv)
|
||||
chacha.update(aad)
|
||||
ciphertext, tag = chacha.encrypt_and_digest(plaintext)
|
||||
return ciphertext, tag
|
||||
|
||||
def decrypt(self, ciphertext: bytes, tag: bytes, cek: bytes, iv: bytes, aad: bytes) -> bytes:
|
||||
"""Key Decryption with AEAD_CHACHA20_POLY1305."""
|
||||
chacha = ChaCha20_Poly1305.new(key=cek, nonce=iv)
|
||||
chacha.update(aad)
|
||||
return chacha.decrypt_and_verify(ciphertext, tag)
|
||||
|
||||
|
||||
C20P = ChaCha20EncModel("C20P", "ChaCha20-Poly1305", 96)
|
||||
XC20P = ChaCha20EncModel("XC20P", "XChaCha20-Poly1305", 192)
|
||||
|
||||
JWE_ENC_MODELS = [C20P, XC20P]
|
||||
|
||||
|
||||
def register_chacha20_poly1305() -> None:
|
||||
for model in JWE_ENC_MODELS:
|
||||
JWERegistry.register(model)
|
||||
@@ -0,0 +1,117 @@
|
||||
from .._rfc7516.models import Recipient, JWEKeyAgreement, JWEKeyWrapping, JWEEncModel
|
||||
from .._rfc7518.jwe_algs import (
|
||||
A128KW,
|
||||
A192KW,
|
||||
A256KW,
|
||||
)
|
||||
from .._rfc7518.ec_key import ECKey
|
||||
from .._rfc7518.derive_key import (
|
||||
derive_key_for_concat_kdf,
|
||||
)
|
||||
from .._rfc7518.jwe_encs import CBCHS2EncModel
|
||||
from ..registry import HeaderParameter
|
||||
from ..errors import InvalidEncryptionAlgorithmError
|
||||
|
||||
|
||||
__all__ = ["ECDH1PUAlgModel", "register_ecdh_1pu", "JWE_ALG_MODELS"]
|
||||
|
||||
|
||||
class ECDH1PUAlgModel(JWEKeyAgreement):
|
||||
"""Key Agreement with Elliptic Curve Diffie-Hellman One-Pass Unified Model (ECDH-1PU)
|
||||
|
||||
https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04
|
||||
"""
|
||||
|
||||
more_header_registry = {
|
||||
"epk": HeaderParameter("Ephemeral Public Key", "jwk", True),
|
||||
"apu": HeaderParameter("Agreement PartyUInfo", "str"),
|
||||
"apv": HeaderParameter("Agreement PartyVInfo", "str"),
|
||||
"skid": HeaderParameter("Sender Key ID", "str"),
|
||||
}
|
||||
key_types = ["EC", "OKP"]
|
||||
tag_aware = True
|
||||
|
||||
def __init__(self, key_wrapping: JWEKeyWrapping | None):
|
||||
if key_wrapping is None:
|
||||
self.name = "ECDH-1PU"
|
||||
self.description = "ECDH-1PU using one-pass KDF and CEK in the Direct Key Agreement mode"
|
||||
self.key_size = None
|
||||
else:
|
||||
self.name = f"ECDH-1PU+{key_wrapping.name}"
|
||||
self.description = f"ECDH-1PU using one-pass KDF and CEK wrapped with {key_wrapping.name}"
|
||||
self.key_size = key_wrapping.key_size
|
||||
self.key_wrapping = key_wrapping
|
||||
|
||||
def _check_enc(self, enc: JWEEncModel) -> None:
|
||||
# https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04#section-2.1
|
||||
# The AES_CBC_HMAC_SHA2 algorithms described in section 5.2 of [RFC7518] are compactly
|
||||
# committing and can be used with ECDH-1PU in Key Agreement with Key Wrapping mode.
|
||||
# Other content encryption algorithms MUST be rejected. In Direct Key Agreement
|
||||
# mode, any JWE content encryption algorithm MAY be used.
|
||||
if self.key_wrapping and not isinstance(enc, CBCHS2EncModel):
|
||||
description = (
|
||||
"In key agreement with key wrapping mode ECDH-1PU algorithm "
|
||||
"only supports AES_CBC_HMAC_SHA2 family encryption algorithms"
|
||||
)
|
||||
raise InvalidEncryptionAlgorithmError(description)
|
||||
|
||||
def encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
self._check_enc(enc)
|
||||
return self.__encrypt_agreed_upon_key(enc, recipient, None)
|
||||
|
||||
def encrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
|
||||
self._check_enc(enc)
|
||||
return self.__encrypt_agreed_upon_key(enc, recipient, tag)
|
||||
|
||||
def decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey]) -> bytes:
|
||||
return self.__decrypt_agreed_upon_key(enc, recipient, None)
|
||||
|
||||
def decrypt_agreed_upon_key_with_tag(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes) -> bytes:
|
||||
return self.__decrypt_agreed_upon_key(enc, recipient, tag)
|
||||
|
||||
def __encrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes | None) -> bytes:
|
||||
sender_key = recipient.sender_key
|
||||
recipient_key = recipient.recipient_key
|
||||
ephemeral_key = recipient.ephemeral_key
|
||||
assert sender_key is not None
|
||||
assert recipient_key is not None
|
||||
assert ephemeral_key is not None
|
||||
|
||||
sender_shared_key = sender_key.exchange_derive_key(recipient_key)
|
||||
ephemeral_shared_key = ephemeral_key.exchange_derive_key(recipient_key)
|
||||
shared_key = ephemeral_shared_key + sender_shared_key
|
||||
headers = recipient.headers()
|
||||
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size, tag)
|
||||
|
||||
def __decrypt_agreed_upon_key(self, enc: JWEEncModel, recipient: Recipient[ECKey], tag: bytes | None) -> bytes:
|
||||
self._check_enc(enc)
|
||||
headers = recipient.headers()
|
||||
assert "epk" in headers
|
||||
|
||||
sender_key = recipient.sender_key
|
||||
recipient_key = recipient.recipient_key
|
||||
assert sender_key is not None
|
||||
assert recipient_key is not None
|
||||
|
||||
ephemeral_key = recipient_key.import_key(headers["epk"])
|
||||
sender_shared_key = recipient_key.exchange_derive_key(sender_key)
|
||||
ephemeral_shared_key = recipient_key.exchange_derive_key(ephemeral_key)
|
||||
shared_key = ephemeral_shared_key + sender_shared_key
|
||||
return derive_key_for_concat_kdf(shared_key, headers, enc.cek_size, self.key_size, tag)
|
||||
|
||||
|
||||
JWE_ALG_MODELS = [
|
||||
ECDH1PUAlgModel(None), # ECDH-1PU
|
||||
ECDH1PUAlgModel(A128KW), # ECDH-1PU+A128KW
|
||||
ECDH1PUAlgModel(A192KW), # ECDH-1PU+A192KW
|
||||
ECDH1PUAlgModel(A256KW), # ECDH-1PU+A256KW
|
||||
]
|
||||
|
||||
|
||||
def register_ecdh_1pu() -> None:
|
||||
from ..jwe import JWERegistry
|
||||
from ..jwk import KeySet
|
||||
|
||||
for model in JWE_ALG_MODELS:
|
||||
JWERegistry.register(model)
|
||||
KeySet.algorithm_keys[model.name] = model.key_types
|
||||
@@ -0,0 +1,238 @@
|
||||
class SecurityWarning(UserWarning):
|
||||
"""Base class for warnings of security issues."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JoseError(Exception):
|
||||
"""Base Exception for all errors in joserfc."""
|
||||
|
||||
#: short-string error code
|
||||
error: str = ""
|
||||
#: long-string to describe this error
|
||||
description: str = ""
|
||||
|
||||
def __init__(self, description: str | None = None):
|
||||
if description is not None:
|
||||
self.description = description
|
||||
|
||||
message = "{}: {}".format(self.error, self.description)
|
||||
super(JoseError, self).__init__(message)
|
||||
|
||||
|
||||
# --- Key related errors --- #
|
||||
|
||||
|
||||
class KeyParameterError(JoseError):
|
||||
error = "key_parameter"
|
||||
|
||||
|
||||
class MissingKeyError(JoseError):
|
||||
error = "missing_key"
|
||||
|
||||
|
||||
class UnsupportedKeyUseError(KeyParameterError):
|
||||
error = "unsupported_key_use"
|
||||
|
||||
|
||||
class UnsupportedKeyAlgorithmError(KeyParameterError):
|
||||
error = "unsupported_key_alg"
|
||||
|
||||
|
||||
class UnsupportedKeyOperationError(KeyParameterError):
|
||||
error = "unsupported_key_operation"
|
||||
|
||||
|
||||
class MissingKeyTypeError(KeyParameterError):
|
||||
error = "missing_key_type"
|
||||
|
||||
|
||||
class InvalidKeyTypeError(KeyParameterError):
|
||||
error = "invalid_key_type"
|
||||
|
||||
|
||||
class InvalidKeyIdError(JoseError):
|
||||
"""This error is designed for Key Set. It is raised when a key
|
||||
can not be found with the given key ID."""
|
||||
|
||||
error = "invalid_key_id"
|
||||
|
||||
|
||||
class InvalidExchangeKeyError(JoseError):
|
||||
"""This error is designed for EC and OKP keys. It is raised when
|
||||
exchanging derive key failed."""
|
||||
|
||||
error = "invalid_exchange_key"
|
||||
description = "Invalid key for exchanging shared key"
|
||||
|
||||
|
||||
# --- JWS & JWE related errors --- #
|
||||
|
||||
|
||||
class DecodeError(JoseError):
|
||||
"""This error is designed for both JWS and JWE. It is raised when deserialization
|
||||
and decryption fails.
|
||||
"""
|
||||
|
||||
error = "decode_error"
|
||||
|
||||
|
||||
class MissingAlgorithmError(JoseError):
|
||||
"""Raised when an algorithm ("alg") is missing."""
|
||||
|
||||
error = "missing_algorithm"
|
||||
description = "Missing 'alg' value in header"
|
||||
|
||||
|
||||
class ConflictAlgorithmError(JoseError):
|
||||
error = "conflict_algorithm"
|
||||
|
||||
|
||||
class UnsupportedAlgorithmError(JoseError):
|
||||
"""This error is designed for both JWS and JWE. It is raised when the
|
||||
given algorithm is not supported in the registry.
|
||||
"""
|
||||
|
||||
error = "unsupported_algorithm"
|
||||
|
||||
|
||||
class InvalidHeaderValueError(JoseError):
|
||||
"""Raised when the given header's value is invalid."""
|
||||
|
||||
error = "invalid_header_value"
|
||||
|
||||
|
||||
class UnsupportedHeaderError(JoseError):
|
||||
"""Raised when an unsupported header is encountered."""
|
||||
|
||||
error = "unsupported_header"
|
||||
|
||||
|
||||
class MissingHeaderError(JoseError):
|
||||
"""This error happens when the required header does not exist."""
|
||||
|
||||
error = "missing_header"
|
||||
|
||||
def __init__(self, key: str):
|
||||
description = f"Missing '{key}' value in header"
|
||||
super(MissingHeaderError, self).__init__(description=description)
|
||||
|
||||
|
||||
class MissingCritHeaderError(JoseError):
|
||||
"""This error happens when the critical header does not exist."""
|
||||
|
||||
error = "missing_crit_header"
|
||||
|
||||
def __init__(self, key: str):
|
||||
description = f"Missing critical '{key}' value in header"
|
||||
super(MissingCritHeaderError, self).__init__(description=description)
|
||||
|
||||
|
||||
class MissingEncryptionError(JoseError):
|
||||
"""This error is designed for JWE. It is raised when the 'enc' value
|
||||
in header is missing."""
|
||||
|
||||
error = "missing_encryption"
|
||||
description = "Missing 'enc' value in header"
|
||||
|
||||
|
||||
class InvalidKeyCurveError(JoseError):
|
||||
"""This error is designed for JWS. It is raised when key's
|
||||
curve name does not match with the given algorithm.
|
||||
"""
|
||||
|
||||
error = "invalid_key_curve"
|
||||
|
||||
|
||||
class InvalidKeyLengthError(JoseError):
|
||||
"""This error is designed for JWE. It is raised when key's
|
||||
length does not align with the given algorithm.
|
||||
"""
|
||||
|
||||
error = "invalid_key_length"
|
||||
|
||||
|
||||
class BadSignatureError(JoseError):
|
||||
"""This error is designed for JWS. It is raised when signature
|
||||
does not match.
|
||||
"""
|
||||
|
||||
error = "bad_signature"
|
||||
|
||||
|
||||
class ExceededSizeError(JoseError):
|
||||
"""This error is designed for validating the token's content size.
|
||||
It raised when the data exceeds the maximum allowed length."""
|
||||
|
||||
error = "exceeded_size"
|
||||
|
||||
|
||||
class InvalidEncryptionAlgorithmError(JoseError):
|
||||
"""This error is designed for JWE. It is raised when "enc" value
|
||||
does not work together with "alg" value.
|
||||
"""
|
||||
|
||||
error = "invalid_encryption_algorithm"
|
||||
|
||||
|
||||
class InvalidEncryptedKeyError(JoseError):
|
||||
error = "invalid_encrypted_key"
|
||||
description = "JWE Encrypted Key value SHOULD be an empty octet sequence"
|
||||
|
||||
|
||||
# --- JWT related errors --- #
|
||||
|
||||
|
||||
class ClaimError(JoseError):
|
||||
"""This a base error for JWT claims validation."""
|
||||
|
||||
claim: str
|
||||
description = "Error claim: '{}'"
|
||||
|
||||
def __init__(self, claim: str, description: str | None = None):
|
||||
self.claim = claim
|
||||
if description is None:
|
||||
description = self.description.format(claim)
|
||||
super(ClaimError, self).__init__(description=description)
|
||||
|
||||
|
||||
class InvalidClaimError(ClaimError):
|
||||
"""This error is designed for JWT. It raised when the claim contains
|
||||
invalid values or types."""
|
||||
|
||||
error = "invalid_claim"
|
||||
description = "Invalid claim: '{}'"
|
||||
|
||||
|
||||
class MissingClaimError(ClaimError):
|
||||
"""This error is designed for JWT. It raised when the required
|
||||
claims are missing."""
|
||||
|
||||
error = "missing_claim"
|
||||
description = "Missing claim: '{}'"
|
||||
|
||||
|
||||
class InsecureClaimError(ClaimError):
|
||||
"""This error is designed for JWT. It raised when the claim
|
||||
contains sensitive information."""
|
||||
|
||||
error = "insecure_claim"
|
||||
description = "Insecure claim: '{}'"
|
||||
|
||||
|
||||
class ExpiredTokenError(ClaimError):
|
||||
"""This error is designed for JWT. It raised when the token is expired."""
|
||||
|
||||
error = "expired_token"
|
||||
description = "The token is expired"
|
||||
|
||||
|
||||
class InvalidPayloadError(JoseError):
|
||||
"""This error is designed for JWT. It raised when the payload is
|
||||
not a valid JSON object."""
|
||||
|
||||
error = "invalid_payload"
|
||||
|
||||
|
||||
# compatibility
|
||||
InvalidTokenError = InvalidClaimError
|
||||
@@ -0,0 +1,100 @@
|
||||
from ._rfc7515.registry import JWSRegistry
|
||||
from ._rfc7515.model import JWSAlgModel
|
||||
from ._rfc7516.registry import JWERegistry
|
||||
from ._rfc7516.models import (
|
||||
JWEDirectEncryption,
|
||||
JWEKeyEncryption,
|
||||
JWEKeyWrapping,
|
||||
JWEKeyAgreement,
|
||||
JWEAlgModel,
|
||||
JWEEncModel,
|
||||
JWEZipModel,
|
||||
)
|
||||
from ._rfc7518.jws_algs import (
|
||||
NoneAlgorithm,
|
||||
HMACAlgorithm,
|
||||
RSAAlgorithm,
|
||||
ESAlgorithm,
|
||||
RSAPSSAlgorithm,
|
||||
JWS_ALGORITHMS as RFC7518_JWS_ALGORITHMS,
|
||||
)
|
||||
from ._rfc7518.jwe_algs import (
|
||||
DirectAlgEncryption,
|
||||
AESAlgKeyWrapping,
|
||||
ECDHESAlgKeyAgreement,
|
||||
AESGCMAlgKeyWrapping,
|
||||
PBES2HSAlgKeyEncryption,
|
||||
JWE_ALG_MODELS,
|
||||
)
|
||||
from ._rfc7518.jwe_encs import (
|
||||
CBCHS2EncModel,
|
||||
GCMEncModel,
|
||||
JWE_ENC_MODELS,
|
||||
)
|
||||
from ._rfc7518.jwe_zips import (
|
||||
DeflateZipModel,
|
||||
JWE_ZIP_MODELS,
|
||||
)
|
||||
from ._rfc8037.jws_eddsa import EdDSA, EdDSAAlgorithm
|
||||
from ._rfc8812 import ES256K
|
||||
from ._rfc9864 import JWS_ALGORITHMS as RFC9864_JWS_ALGORITHMS
|
||||
from ._keys import KeySet
|
||||
|
||||
__all__ = [
|
||||
# JWS algorithms
|
||||
"JWS_ALGORITHMS",
|
||||
"JWSAlgModel",
|
||||
"NoneAlgorithm",
|
||||
"HMACAlgorithm",
|
||||
"RSAAlgorithm",
|
||||
"ESAlgorithm",
|
||||
"RSAPSSAlgorithm",
|
||||
"EdDSAAlgorithm",
|
||||
# JWE algorithms
|
||||
"JWE_ALG_MODELS",
|
||||
"JWE_ENC_MODELS",
|
||||
"JWE_ZIP_MODELS",
|
||||
"JWEAlgModel",
|
||||
"JWEDirectEncryption",
|
||||
"JWEKeyEncryption",
|
||||
"JWEKeyWrapping",
|
||||
"JWEKeyAgreement",
|
||||
"DirectAlgEncryption",
|
||||
"AESAlgKeyWrapping",
|
||||
"ECDHESAlgKeyAgreement",
|
||||
"AESGCMAlgKeyWrapping",
|
||||
"PBES2HSAlgKeyEncryption",
|
||||
"JWEEncModel",
|
||||
"CBCHS2EncModel",
|
||||
"GCMEncModel",
|
||||
"JWEZipModel",
|
||||
"DeflateZipModel",
|
||||
# setup methods
|
||||
"setup_jws_algorithms",
|
||||
"setup_jwe_algorithms",
|
||||
]
|
||||
|
||||
JWS_ALGORITHMS = [
|
||||
*RFC7518_JWS_ALGORITHMS,
|
||||
EdDSA,
|
||||
ES256K,
|
||||
*RFC9864_JWS_ALGORITHMS,
|
||||
]
|
||||
|
||||
|
||||
def setup_jws_algorithms() -> None:
|
||||
for _alg in JWS_ALGORITHMS:
|
||||
JWSRegistry.register(_alg)
|
||||
KeySet.algorithm_keys[_alg.name] = [_alg.key_type]
|
||||
|
||||
|
||||
def setup_jwe_algorithms() -> None:
|
||||
for _alg in JWE_ALG_MODELS:
|
||||
KeySet.algorithm_keys[_alg.name] = _alg.key_types
|
||||
JWERegistry.register(_alg)
|
||||
|
||||
for _enc in JWE_ENC_MODELS:
|
||||
JWERegistry.register(_enc)
|
||||
|
||||
for _zip in JWE_ZIP_MODELS:
|
||||
JWERegistry.register(_zip)
|
||||
@@ -0,0 +1,277 @@
|
||||
from typing import cast, overload, Union
|
||||
from collections.abc import Collection
|
||||
from ._rfc7516.types import (
|
||||
GeneralJSONSerialization,
|
||||
FlattenedJSONSerialization,
|
||||
)
|
||||
from ._rfc7516.models import (
|
||||
Recipient,
|
||||
CompactEncryption,
|
||||
GeneralJSONEncryption,
|
||||
FlattenedJSONEncryption,
|
||||
)
|
||||
from ._rfc7516.registry import (
|
||||
JWERegistry,
|
||||
default_registry,
|
||||
)
|
||||
from ._rfc7516.message import perform_encrypt, perform_decrypt
|
||||
from ._rfc7516.compact import represent_compact, extract_compact
|
||||
from ._rfc7516.json import (
|
||||
represent_general_json,
|
||||
represent_flattened_json,
|
||||
extract_general_json,
|
||||
extract_flattened_json,
|
||||
)
|
||||
from .jwa import setup_jwe_algorithms
|
||||
from .jwk import Key, KeySet, ECKey, OKPKey, KeyFlexible, guess_key
|
||||
from .util import to_bytes
|
||||
from .registry import Header, reject_unprotected_crit_header
|
||||
|
||||
__all__ = [
|
||||
# types
|
||||
"GeneralJSONSerialization",
|
||||
"FlattenedJSONSerialization",
|
||||
# modules
|
||||
"JWERegistry",
|
||||
"Recipient",
|
||||
"CompactEncryption",
|
||||
"GeneralJSONEncryption",
|
||||
"FlattenedJSONEncryption",
|
||||
# methods
|
||||
"encrypt_compact",
|
||||
"decrypt_compact",
|
||||
"encrypt_json",
|
||||
"decrypt_json",
|
||||
# consts
|
||||
"default_registry",
|
||||
]
|
||||
setup_jwe_algorithms()
|
||||
|
||||
|
||||
def encrypt_compact(
|
||||
protected: Header,
|
||||
plaintext: bytes | str,
|
||||
public_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> str:
|
||||
"""Generate a JWE Compact Serialization. The JWE Compact Serialization represents
|
||||
encrypted content as a compact, URL-safe string. This string is::
|
||||
|
||||
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
|
||||
BASE64URL(JWE Encrypted Key) || '.' ||
|
||||
BASE64URL(JWE Initialization Vector) || '.' ||
|
||||
BASE64URL(JWE Ciphertext) || '.' ||
|
||||
BASE64URL(JWE Authentication Tag)
|
||||
|
||||
:param protected: protected header part of the JWE, in dict
|
||||
:param plaintext: the content (message) to be encrypted
|
||||
:param public_key: a public key used to encrypt the CEK
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWERegistry to use
|
||||
:param sender_key: only required when using ECDH-1PU
|
||||
:return: JWE Compact Serialization in bytes
|
||||
"""
|
||||
|
||||
if algorithms:
|
||||
registry = JWERegistry(algorithms=algorithms)
|
||||
elif registry is None:
|
||||
registry = default_registry
|
||||
|
||||
obj = CompactEncryption(protected, to_bytes(plaintext))
|
||||
recipient: Recipient[Key] = Recipient(obj)
|
||||
key = guess_key(public_key, recipient, True, use="enc")
|
||||
key.check_use("enc")
|
||||
recipient.recipient_key = key
|
||||
if sender_key:
|
||||
recipient.sender_key = _guess_sender_key(recipient, sender_key, True)
|
||||
obj.recipient = recipient
|
||||
perform_encrypt(obj, registry)
|
||||
out = represent_compact(obj)
|
||||
return out.decode("utf-8")
|
||||
|
||||
|
||||
def decrypt_compact(
|
||||
value: bytes | str,
|
||||
private_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> CompactEncryption:
|
||||
"""Extract and validate the JWE Compact Serialization (in string, or bytes)
|
||||
with the given key. An JWE Compact Serialization looks like:
|
||||
|
||||
.. code-block:: text
|
||||
:caption: line breaks for display purposes only
|
||||
|
||||
OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe
|
||||
ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb
|
||||
Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV
|
||||
mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8
|
||||
1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi
|
||||
6UklfCpIMfIjf7iGdXKHzg
|
||||
|
||||
:param value: a string (or bytes) of the JWE Compact Serialization
|
||||
:param private_key: a flexible private key to decrypt the serialization
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWERegistry to use
|
||||
:param sender_key: only required when using ECDH-1PU
|
||||
:return: object of the ``CompactEncryption``
|
||||
"""
|
||||
if algorithms:
|
||||
registry = JWERegistry(algorithms=algorithms)
|
||||
elif registry is None:
|
||||
registry = default_registry
|
||||
|
||||
obj = extract_compact(to_bytes(value), registry)
|
||||
recipient = obj.recipient
|
||||
assert recipient is not None
|
||||
key = guess_key(private_key, recipient, use="enc")
|
||||
key.check_use("enc")
|
||||
recipient.recipient_key = key
|
||||
if sender_key:
|
||||
recipient.sender_key = _guess_sender_key(recipient, sender_key)
|
||||
perform_decrypt(obj, registry)
|
||||
return obj
|
||||
|
||||
|
||||
@overload
|
||||
def encrypt_json(
|
||||
obj: GeneralJSONEncryption,
|
||||
public_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> GeneralJSONSerialization: ...
|
||||
|
||||
|
||||
@overload
|
||||
def encrypt_json(
|
||||
obj: FlattenedJSONEncryption,
|
||||
public_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> FlattenedJSONSerialization: ...
|
||||
|
||||
|
||||
def encrypt_json(
|
||||
obj: GeneralJSONEncryption | FlattenedJSONEncryption,
|
||||
public_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> GeneralJSONSerialization | FlattenedJSONSerialization:
|
||||
"""Generate a JWE JSON Serialization (in dict). The JWE JSON Serialization
|
||||
represents encrypted content as a JSON object. This representation is neither
|
||||
optimized for compactness nor URL safe.
|
||||
|
||||
When calling this method, developers MUST construct an instance of a
|
||||
``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` object. Here
|
||||
is an example::
|
||||
|
||||
from joserfc.jwe import GeneralJSONEncryption
|
||||
|
||||
protected = {"enc": "A128CBC-HS256"}
|
||||
plaintext = b"hello world"
|
||||
header = {"jku": "https://server.example.com/keys.jwks"} # optional shared header
|
||||
obj = GeneralJSONEncryption(protected, plaintext, header)
|
||||
# add the recipients
|
||||
obj.add_recipient({"kid": "alice", "alg": "RSA1_5"}) # not configured a key
|
||||
bob_key = OctKey.import_key("bob secret")
|
||||
obj.add_recipient({"kid": "bob", "alg": "A128KW"}, bob_key)
|
||||
|
||||
:param obj: an instance of ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption``
|
||||
:param public_key: a public key used to encrypt the CEK
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWERegistry to use
|
||||
:param sender_key: only required when using ECDH-1PU
|
||||
:return: JWE JSON Serialization in dict
|
||||
"""
|
||||
|
||||
if algorithms:
|
||||
registry = JWERegistry(algorithms=algorithms)
|
||||
elif registry is None:
|
||||
registry = default_registry
|
||||
|
||||
reject_unprotected_crit_header(obj.unprotected)
|
||||
for recipient in obj.recipients:
|
||||
if sender_key and not recipient.sender_key:
|
||||
recipient.sender_key = _guess_sender_key(recipient, sender_key, True)
|
||||
if not recipient.recipient_key:
|
||||
assert public_key is not None
|
||||
key = guess_key(public_key, recipient, True, use="enc")
|
||||
key.check_use("enc")
|
||||
recipient.recipient_key = key
|
||||
|
||||
perform_encrypt(obj, registry)
|
||||
if isinstance(obj, GeneralJSONEncryption):
|
||||
return represent_general_json(obj)
|
||||
return represent_flattened_json(obj)
|
||||
|
||||
|
||||
def decrypt_json(
|
||||
data: GeneralJSONSerialization | FlattenedJSONSerialization,
|
||||
private_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWERegistry | None = None,
|
||||
sender_key: ECKey | OKPKey | KeySet | None = None,
|
||||
) -> GeneralJSONEncryption | FlattenedJSONEncryption:
|
||||
"""Decrypt the JWE JSON Serialization (in dict) to a
|
||||
``GeneralJSONEncryption`` or ``FlattenedJSONEncryption`` object.
|
||||
|
||||
:param data: JWE JSON Serialization in dict
|
||||
:param private_key: a flexible private key to decrypt the CEK
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWERegistry to use
|
||||
:param sender_key: only required when using ECDH-1PU
|
||||
:return: an instance of ``GeneralJSONEncryption`` or ``FlattenedJSONEncryption``
|
||||
"""
|
||||
if algorithms:
|
||||
registry = JWERegistry(algorithms=algorithms)
|
||||
elif registry is None:
|
||||
registry = default_registry
|
||||
|
||||
reject_unprotected_crit_header(data.get("unprotected"))
|
||||
if "recipients" in data:
|
||||
general_obj = extract_general_json(cast(GeneralJSONSerialization, data), registry)
|
||||
_attach_recipient_keys(general_obj.recipients, private_key, sender_key)
|
||||
perform_decrypt(general_obj, registry)
|
||||
return general_obj
|
||||
else:
|
||||
flattened_obj = extract_flattened_json(cast(FlattenedJSONSerialization, data), registry)
|
||||
_attach_recipient_keys(flattened_obj.recipients, private_key, sender_key)
|
||||
perform_decrypt(flattened_obj, registry)
|
||||
return flattened_obj
|
||||
|
||||
|
||||
def _attach_recipient_keys(
|
||||
recipients: list[Recipient[Key]], private_key: KeyFlexible, sender_key: ECKey | OKPKey | KeySet | None = None
|
||||
) -> None:
|
||||
for recipient in recipients:
|
||||
key = guess_key(private_key, recipient, use="enc")
|
||||
key.check_use("enc")
|
||||
recipient.recipient_key = key
|
||||
if sender_key:
|
||||
recipient.sender_key = _guess_sender_key(recipient, sender_key)
|
||||
|
||||
|
||||
def _guess_sender_key(
|
||||
recipient: Recipient[Key], key: ECKey | OKPKey | KeySet, use_random: bool = False
|
||||
) -> ECKey | OKPKey:
|
||||
if not isinstance(key, KeySet):
|
||||
return key
|
||||
|
||||
headers = recipient.headers()
|
||||
skid = headers.get("skid")
|
||||
if skid:
|
||||
skey = cast(Union[ECKey, OKPKey], key.get_by_kid(skid))
|
||||
return skey
|
||||
|
||||
if use_random:
|
||||
skey = cast(Union[ECKey, OKPKey], key.pick_random_key(headers["alg"]))
|
||||
if skey is not None:
|
||||
recipient.add_header("skid", skey.kid)
|
||||
return skey
|
||||
raise ValueError("Invalid key")
|
||||
@@ -0,0 +1,216 @@
|
||||
import warnings
|
||||
from typing import cast, overload, Literal, Union, Callable, Protocol
|
||||
from ._keys import (
|
||||
JWKRegistry,
|
||||
KeySet,
|
||||
Key,
|
||||
KeySetSerialization,
|
||||
)
|
||||
from ._rfc7517.pem import import_from_pem_key, import_from_ssh_key
|
||||
from ._rfc7517.types import AnyKey, DictKey, KeyParameters
|
||||
from ._rfc7518.oct_key import OctKey
|
||||
from ._rfc7518.rsa_key import RSAKey
|
||||
from ._rfc7518.ec_key import ECKey
|
||||
from ._rfc8037.okp_key import OKPKey
|
||||
from ._rfc8812 import register_secp256k1
|
||||
from ._rfc7638 import calculate_thumbprint as thumbprint
|
||||
from ._rfc9278 import calculate_thumbprint_uri as thumbprint_uri
|
||||
from .errors import SecurityWarning, InvalidKeyTypeError
|
||||
from .registry import Header
|
||||
from .util import to_bytes
|
||||
|
||||
|
||||
__all__ = [
|
||||
# types
|
||||
"Key",
|
||||
"DictKey",
|
||||
"KeyParameters",
|
||||
"KeyCallable",
|
||||
"KeyFlexible",
|
||||
"KeySetSerialization",
|
||||
"KeyBase",
|
||||
"GuestProtocol",
|
||||
# modules
|
||||
"JWKRegistry",
|
||||
"OctKey",
|
||||
"RSAKey",
|
||||
"ECKey",
|
||||
"OKPKey",
|
||||
"KeySet",
|
||||
# methods
|
||||
"guess_key",
|
||||
"import_key",
|
||||
"generate_key",
|
||||
"thumbprint",
|
||||
"thumbprint_uri",
|
||||
]
|
||||
|
||||
register_secp256k1()
|
||||
|
||||
|
||||
class GuestProtocol(Protocol): # pragma: no cover
|
||||
def headers(self) -> Header: ...
|
||||
|
||||
def set_kid(self, kid: str) -> None: ...
|
||||
|
||||
|
||||
KeyBase = Union[Key, KeySet]
|
||||
KeyCallable = Callable[[GuestProtocol], KeyBase]
|
||||
KeyFlexible = Union[KeyBase, KeyCallable]
|
||||
|
||||
|
||||
def guess_key(
|
||||
key: KeyFlexible,
|
||||
obj: GuestProtocol,
|
||||
random: bool = False,
|
||||
use: Literal["sig", "enc"] | None = None,
|
||||
) -> Key:
|
||||
"""Guess key from a various sources.
|
||||
|
||||
:param key: a very flexible key
|
||||
:param obj: a protocol that has ``headers`` and ``set_kid`` methods
|
||||
:param random: pick a random key from key set
|
||||
:param use: optional "use" value
|
||||
"""
|
||||
resolved_key: KeyBase
|
||||
if callable(key):
|
||||
resolved_key = key(obj)
|
||||
else:
|
||||
resolved_key = key
|
||||
|
||||
if isinstance(resolved_key, (OctKey, RSAKey, ECKey, OKPKey)):
|
||||
return resolved_key
|
||||
elif isinstance(resolved_key, KeySet):
|
||||
headers = obj.headers()
|
||||
kid: str | None = headers.get("kid")
|
||||
|
||||
parameters: KeyParameters = {"alg": headers["alg"]}
|
||||
if use:
|
||||
parameters["use"] = use
|
||||
|
||||
if not kid and random:
|
||||
# choose one key by random
|
||||
return_key = resolved_key.pick_random_key(headers["alg"], parameters)
|
||||
if return_key is None:
|
||||
raise ValueError("Invalid key")
|
||||
return_key.ensure_kid()
|
||||
obj.set_kid(cast(str, return_key.kid))
|
||||
else:
|
||||
return_key = resolved_key.get_by_kid(kid, parameters)
|
||||
return return_key
|
||||
else:
|
||||
raise ValueError("Invalid key")
|
||||
|
||||
|
||||
@overload
|
||||
def import_key(data: AnyKey, key_type: Literal["oct"], parameters: KeyParameters | None = None) -> OctKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def import_key(data: AnyKey, key_type: Literal["RSA"], parameters: KeyParameters | None = None) -> RSAKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def import_key(data: AnyKey, key_type: Literal["EC"], parameters: KeyParameters | None = None) -> ECKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def import_key(data: AnyKey, key_type: Literal["OKP"], parameters: KeyParameters | None = None) -> OKPKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def import_key(data: AnyKey, key_type: None = None, parameters: KeyParameters | None = None) -> Key: ...
|
||||
|
||||
|
||||
def import_key(
|
||||
data: AnyKey,
|
||||
key_type: Literal["oct", "RSA", "EC", "OKP"] | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
) -> Key:
|
||||
"""Importing a key from bytes, string, and dict. When ``value`` is a dict,
|
||||
this method can tell the key type automatically, otherwise, developers
|
||||
SHOULD pass the ``key_type`` themselves.
|
||||
|
||||
:param data: the key data in bytes, string, or dict.
|
||||
:param key_type: an optional key type in string.
|
||||
:param parameters: extra key parameters
|
||||
:return: OctKey, RSAKey, ECKey, or OKPKey
|
||||
"""
|
||||
if isinstance(data, (str, bytes)) and key_type is None:
|
||||
warnings.warn("Using implicit key type is not recommended.", SecurityWarning)
|
||||
|
||||
value = to_bytes(data)
|
||||
ssh_types = tuple(
|
||||
cls.binding.ssh_type for cls in JWKRegistry.key_types.values() if hasattr(cls.binding, "ssh_type")
|
||||
)
|
||||
if value.startswith(ssh_types):
|
||||
try:
|
||||
raw_key = import_from_ssh_key(value)
|
||||
except ValueError:
|
||||
return OctKey.import_key(value, parameters)
|
||||
else:
|
||||
try:
|
||||
raw_key = import_from_pem_key(value)
|
||||
except ValueError:
|
||||
return OctKey.import_key(value, parameters)
|
||||
|
||||
for cls in JWKRegistry.key_types.values():
|
||||
if hasattr(cls.binding, "check_cryptography_key") and cls.binding.check_cryptography_key(raw_key):
|
||||
return cls(raw_key, data, parameters)
|
||||
raise InvalidKeyTypeError("Not a key of any supported type") # pragma: no cover
|
||||
return JWKRegistry.import_key(data, key_type, parameters)
|
||||
|
||||
|
||||
@overload
|
||||
def generate_key(
|
||||
key_type: Literal["oct"],
|
||||
crv_or_size: int | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> OctKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def generate_key(
|
||||
key_type: Literal["RSA"],
|
||||
crv_or_size: int | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> RSAKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def generate_key(
|
||||
key_type: Literal["EC"],
|
||||
crv_or_size: Literal["P-256", "P-384", "P-521", "secp256k1"] | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> ECKey: ...
|
||||
|
||||
|
||||
@overload
|
||||
def generate_key(
|
||||
key_type: Literal["OKP"],
|
||||
crv_or_size: Literal["Ed25519", "Ed448", "X25519", "X448"] | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> OKPKey: ...
|
||||
|
||||
|
||||
def generate_key(
|
||||
key_type: Literal["oct", "RSA", "EC", "OKP"],
|
||||
crv_or_size: str | int | None = None,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> Key:
|
||||
"""Generating key according to the given key type. When ``key_type`` is
|
||||
"oct" and "RSA", the second parameter SHOULD be a key size in bits.
|
||||
When ``key_type`` is "EC" and "OKP", the second
|
||||
parameter SHOULD be a "crv" string.
|
||||
"""
|
||||
return JWKRegistry.generate_key(key_type, crv_or_size, parameters, private, auto_kid)
|
||||
@@ -0,0 +1,353 @@
|
||||
from typing import overload, Any
|
||||
from collections.abc import Collection
|
||||
from ._rfc7515.model import (
|
||||
JWSAlgModel,
|
||||
HeaderMember,
|
||||
CompactSignature,
|
||||
GeneralJSONSignature,
|
||||
FlattenedJSONSignature,
|
||||
)
|
||||
from ._rfc7515.registry import (
|
||||
JWSRegistry,
|
||||
construct_registry,
|
||||
default_registry,
|
||||
)
|
||||
from ._rfc7515.compact import (
|
||||
sign_compact,
|
||||
verify_compact,
|
||||
detach_compact_content,
|
||||
)
|
||||
from ._rfc7515.json import (
|
||||
sign_general_json,
|
||||
sign_flattened_json,
|
||||
verify_general_json,
|
||||
verify_flattened_json,
|
||||
extract_general_json,
|
||||
detach_json_content,
|
||||
)
|
||||
from ._rfc7515.types import (
|
||||
HeaderDict,
|
||||
GeneralJSONSerialization,
|
||||
FlattenedJSONSerialization,
|
||||
)
|
||||
from ._rfc7797.util import is_rfc7797_enabled
|
||||
from ._rfc7797.compact import (
|
||||
sign_rfc7515_compact,
|
||||
extract_rfc7515_compact as extract_compact,
|
||||
)
|
||||
from ._rfc7797.json import (
|
||||
sign_rfc7797_json,
|
||||
extract_rfc7797_json as extract_flattened_json,
|
||||
)
|
||||
from .errors import BadSignatureError, MissingKeyError
|
||||
from .jwk import Key, KeyFlexible, guess_key
|
||||
from .jwa import setup_jws_algorithms
|
||||
from .util import to_bytes
|
||||
from .registry import Header
|
||||
|
||||
__all__ = [
|
||||
# types
|
||||
"HeaderDict",
|
||||
"GeneralJSONSerialization",
|
||||
"FlattenedJSONSerialization",
|
||||
# modules
|
||||
"JWSRegistry",
|
||||
"HeaderMember",
|
||||
"CompactSignature",
|
||||
"GeneralJSONSignature",
|
||||
"FlattenedJSONSignature",
|
||||
# methods
|
||||
"serialize_compact",
|
||||
"deserialize_compact",
|
||||
"extract_compact",
|
||||
"validate_compact",
|
||||
"serialize_json",
|
||||
"deserialize_json",
|
||||
"detach_content",
|
||||
# consts
|
||||
"default_registry",
|
||||
]
|
||||
|
||||
setup_jws_algorithms()
|
||||
|
||||
|
||||
def serialize_compact(
|
||||
protected: Header,
|
||||
payload: bytes | str,
|
||||
private_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> str:
|
||||
"""Generate a JWS Compact Serialization. The JWS Compact Serialization
|
||||
represents digitally signed or MACed content as a compact, URL-safe
|
||||
string, per Section 7.1.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
BASE64URL(UTF8(JWS Protected Header)) || '.' ||
|
||||
BASE64URL(JWS Payload) || '.' ||
|
||||
BASE64URL(JWS Signature)
|
||||
|
||||
:param protected: protected header part of the JWS, in dict
|
||||
:param payload: payload data of the JWS, in bytes
|
||||
:param private_key: a flexible private key to sign the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWSRegistry to use
|
||||
:return: JWS in str
|
||||
"""
|
||||
if registry is None:
|
||||
registry = construct_registry(algorithms)
|
||||
|
||||
registry.check_header(protected)
|
||||
|
||||
is_rfc7797 = is_rfc7797_enabled(protected)
|
||||
obj = CompactSignature(protected, to_bytes(payload))
|
||||
alg: JWSAlgModel = registry.get_alg(protected["alg"])
|
||||
|
||||
# "none" algorithm requires no key
|
||||
key: Key | None = None
|
||||
if alg.name != "none":
|
||||
if private_key is None:
|
||||
raise MissingKeyError()
|
||||
|
||||
key = guess_key(private_key, obj, True, use="sig")
|
||||
alg.check_key(key)
|
||||
|
||||
if is_rfc7797:
|
||||
out = sign_rfc7515_compact(obj, alg, key)
|
||||
else:
|
||||
out = sign_compact(obj, alg, key)
|
||||
return out.decode("utf-8")
|
||||
|
||||
|
||||
def validate_compact(
|
||||
obj: CompactSignature,
|
||||
public_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> bool:
|
||||
"""Validate the JWS Compact Serialization with the given key.
|
||||
This method is usually used together with ``extract_compact``.
|
||||
|
||||
:param obj: object of the JWS Compact Serialization
|
||||
:param public_key: a flexible public key to verify the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWSRegistry to use
|
||||
"""
|
||||
if registry is None:
|
||||
registry = construct_registry(algorithms)
|
||||
|
||||
headers = obj.headers()
|
||||
registry.check_header(headers)
|
||||
alg: JWSAlgModel = registry.get_alg(headers["alg"])
|
||||
|
||||
# "none" algorithm requires no key
|
||||
if headers["alg"] == "none":
|
||||
return verify_compact(obj, alg, None)
|
||||
|
||||
if public_key is None:
|
||||
raise MissingKeyError()
|
||||
|
||||
key: Key = guess_key(public_key, obj, use="sig")
|
||||
alg.check_key(key)
|
||||
return verify_compact(obj, alg, key)
|
||||
|
||||
|
||||
def deserialize_compact(
|
||||
value: bytes | str,
|
||||
public_key: KeyFlexible | None,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
payload: bytes | str | None = None,
|
||||
) -> CompactSignature:
|
||||
"""Extract and validate the JWS Compact Serialization (in string, or bytes)
|
||||
with the given key. An JWE Compact Serialization looks like:
|
||||
|
||||
.. code-block:: text
|
||||
:caption: line breaks for display purposes only
|
||||
|
||||
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
|
||||
.
|
||||
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
|
||||
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
|
||||
.
|
||||
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
|
||||
|
||||
:param value: a string (or bytes) of the JWS Compact Serialization
|
||||
:param public_key: a flexible public key to verify the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWSRegistry to use
|
||||
:param payload: optional payload, required with detached content
|
||||
:raises BadSignatureError: when signature verification fails
|
||||
:return: object of the CompactSignature
|
||||
"""
|
||||
obj = extract_compact(to_bytes(value), payload, registry)
|
||||
if not validate_compact(obj, public_key, algorithms, registry):
|
||||
raise BadSignatureError()
|
||||
return obj
|
||||
|
||||
|
||||
@overload
|
||||
def serialize_json(
|
||||
members: list[HeaderDict],
|
||||
payload: bytes | str,
|
||||
private_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> GeneralJSONSerialization: ...
|
||||
|
||||
|
||||
@overload
|
||||
def serialize_json(
|
||||
members: HeaderDict,
|
||||
payload: bytes | str,
|
||||
private_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> FlattenedJSONSerialization: ...
|
||||
|
||||
|
||||
def serialize_json(
|
||||
members: HeaderDict | list[HeaderDict],
|
||||
payload: bytes | str,
|
||||
private_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> GeneralJSONSerialization | FlattenedJSONSerialization:
|
||||
"""Generate a JWS JSON Serialization (in dict). The JWS JSON Serialization
|
||||
represents digitally signed or MACed content as a JSON object. This representation
|
||||
is neither optimized for compactness nor URL-safe.
|
||||
|
||||
A general JWS JSON Serialization contains:
|
||||
|
||||
payload
|
||||
The "payload" member MUST be present and contain the value
|
||||
BASE64URL(JWS Payload).
|
||||
|
||||
signatures
|
||||
The "signatures" member value MUST be an array of JSON objects.
|
||||
Each object represents a signature or MAC over the JWS Payload and
|
||||
the JWS Protected Header.
|
||||
|
||||
A flatten JWS JSON Serialization looks like:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
{
|
||||
"payload":"<payload contents>",
|
||||
"protected":"<integrity-protected header contents>",
|
||||
"header":<non-integrity-protected header contents>,
|
||||
"signature":"<signature contents>"
|
||||
}
|
||||
"""
|
||||
if registry is None:
|
||||
registry = construct_registry(algorithms)
|
||||
|
||||
def find_key(obj: HeaderMember) -> Key:
|
||||
return guess_key(private_key, obj, True, use="sig")
|
||||
|
||||
_payload = to_bytes(payload)
|
||||
if isinstance(members, list):
|
||||
_members = [HeaderMember(**member) for member in members]
|
||||
return sign_general_json(_members, _payload, registry, find_key)
|
||||
else:
|
||||
member = HeaderMember(**members)
|
||||
if is_rfc7797_enabled(member.headers()):
|
||||
return sign_rfc7797_json(member, _payload, registry, find_key)
|
||||
return sign_flattened_json(member, _payload, registry, find_key)
|
||||
|
||||
|
||||
@overload
|
||||
def deserialize_json(
|
||||
value: GeneralJSONSerialization,
|
||||
public_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> GeneralJSONSignature: ...
|
||||
|
||||
|
||||
@overload
|
||||
def deserialize_json(
|
||||
value: FlattenedJSONSerialization,
|
||||
public_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> FlattenedJSONSignature: ...
|
||||
|
||||
|
||||
def deserialize_json(
|
||||
value: GeneralJSONSerialization | FlattenedJSONSerialization,
|
||||
public_key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | None = None,
|
||||
) -> GeneralJSONSignature | FlattenedJSONSignature:
|
||||
"""Extract and validate the JWS (in string) with the given key.
|
||||
|
||||
:param value: a dict of the JSON signature
|
||||
:param public_key: a flexible public key to verify the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a JWSRegistry to use
|
||||
:return: object of GeneralJSONSignature or FlattenedJSONSignature
|
||||
:raises BadSignatureError: when signature verification fails
|
||||
"""
|
||||
if registry is None:
|
||||
registry = construct_registry(algorithms)
|
||||
|
||||
def find_key(obj: HeaderMember) -> Key:
|
||||
return guess_key(public_key, obj, use="sig")
|
||||
|
||||
if "signatures" in value:
|
||||
general_obj = extract_general_json(value, registry)
|
||||
if not verify_general_json(general_obj, registry, find_key):
|
||||
raise BadSignatureError()
|
||||
return general_obj
|
||||
else:
|
||||
flattened_obj = extract_flattened_json(value, registry)
|
||||
if not verify_flattened_json(flattened_obj, registry, find_key):
|
||||
raise BadSignatureError()
|
||||
return flattened_obj
|
||||
|
||||
|
||||
@overload
|
||||
def detach_content(value: str) -> str: ...
|
||||
|
||||
|
||||
@overload
|
||||
def detach_content(value: GeneralJSONSerialization) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def detach_content(value: FlattenedJSONSerialization) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
def detach_content(value: Any) -> Any:
|
||||
"""In some contexts, it is useful to integrity-protect content that is
|
||||
not itself contained in a JWS. This method is an implementation of
|
||||
https://www.rfc-editor.org/rfc/rfc7515#appendix-F
|
||||
|
||||
It is used to detach the content of the compact and JSON serialization.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from joserfc import jws
|
||||
>>> from joserfc.jwk import OctKey
|
||||
>>> key = OctKey.import_key("secret")
|
||||
>>> encoded_text = jws.serialize_compact({"alg": "HS256"}, b"hello", key)
|
||||
>>> jws.detach_content(encoded_text)
|
||||
'eyJhbGciOiJIUzI1NiJ9..UYmO_lPAY5V0Wf4KZsfhiYs1SxqXPhxvjuYqellDV5A'
|
||||
|
||||
You can also detach the JSON serialization:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> obj = jws.serialize_json({"protected": {"alg": "HS256"}}, b"hello", key)
|
||||
>>> jws.detach_content(obj)
|
||||
{
|
||||
'payload': '',
|
||||
'signature': 'UYmO_lPAY5V0Wf4KZsfhiYs1SxqXPhxvjuYqellDV5A',
|
||||
'protected': 'eyJhbGciOiJIUzI1NiJ9'
|
||||
}
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return detach_compact_content(value)
|
||||
return detach_json_content(value)
|
||||
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
from collections.abc import Collection
|
||||
from ._rfc7519.claims import (
|
||||
convert_claims,
|
||||
Claims,
|
||||
ClaimsOption,
|
||||
BaseClaimsRegistry,
|
||||
JWTClaimsRegistry,
|
||||
)
|
||||
from ._rfc7519.security import check_sensitive_data
|
||||
from .jws import (
|
||||
JWSRegistry,
|
||||
serialize_compact,
|
||||
deserialize_compact,
|
||||
)
|
||||
from .jwe import (
|
||||
JWERegistry,
|
||||
encrypt_compact,
|
||||
decrypt_compact,
|
||||
)
|
||||
from .jwk import KeyFlexible
|
||||
from .errors import InvalidPayloadError
|
||||
from .util import to_bytes
|
||||
from .registry import Header
|
||||
|
||||
__all__ = [
|
||||
# types
|
||||
"Claims",
|
||||
"ClaimsOption",
|
||||
# modules
|
||||
"BaseClaimsRegistry",
|
||||
"JWTClaimsRegistry",
|
||||
"Token",
|
||||
# methods
|
||||
"encode",
|
||||
"decode",
|
||||
"check_sensitive_data",
|
||||
]
|
||||
|
||||
|
||||
class Token:
|
||||
"""The extracted token object, which contains ``header`` and ``claims``.
|
||||
|
||||
:param header: the header part of the JWT
|
||||
:param claims: the payload part of the JWT
|
||||
"""
|
||||
|
||||
def __init__(self, header: Header, claims: Claims):
|
||||
#: header in dict
|
||||
self.header = header
|
||||
#: payload claims in dict
|
||||
self.claims = claims
|
||||
|
||||
|
||||
def encode(
|
||||
header: Header,
|
||||
claims: Claims,
|
||||
key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | JWERegistry | None = None,
|
||||
encoder_cls: type[JSONEncoder] | None = None,
|
||||
default_type: str | None = "JWT",
|
||||
) -> str:
|
||||
"""Encode a JSON Web Token with the given header, and claims.
|
||||
|
||||
:param header: A dict of the JWT header
|
||||
:param claims: A dict of the JWT claims to be encoded
|
||||
:param key: key used to sign the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a ``JWSRegistry`` or ``JWERegistry`` to use
|
||||
:param encoder_cls: A JSONEncoder subclass to use
|
||||
:param default_type: default value of the ``typ`` header parameter
|
||||
"""
|
||||
if default_type is not None:
|
||||
_header = {"typ": default_type, **header}
|
||||
else:
|
||||
_header = {**header}
|
||||
payload = convert_claims(claims, encoder_cls)
|
||||
if isinstance(registry, JWERegistry):
|
||||
return encrypt_compact(_header, payload, key, algorithms, registry)
|
||||
else:
|
||||
return serialize_compact(_header, payload, key, algorithms, registry)
|
||||
|
||||
|
||||
def decode(
|
||||
value: bytes | str,
|
||||
key: KeyFlexible,
|
||||
algorithms: Collection[str] | None = None,
|
||||
registry: JWSRegistry | JWERegistry | None = None,
|
||||
decoder_cls: type[JSONDecoder] | None = None,
|
||||
) -> Token:
|
||||
"""Decode the JSON Web Token string with the given key, and validate
|
||||
it with the claims requests.
|
||||
|
||||
:param value: text of the JWT
|
||||
:param key: key used to verify the signature
|
||||
:param algorithms: a collection (list, tuple, or set) of allowed algorithms
|
||||
:param registry: a ``JWSRegistry`` or ``JWERegistry`` to use
|
||||
:param decoder_cls: A JSONDecoder subclass to use
|
||||
:raise BadSignatureError: when signature verification fails
|
||||
:raise InvalidPayloadError: when payload is not a valid JSON object
|
||||
"""
|
||||
_value = to_bytes(value)
|
||||
header: Header
|
||||
payload: bytes
|
||||
if isinstance(registry, JWERegistry):
|
||||
header, payload = _decode_jwe(_value, key, algorithms, registry)
|
||||
else:
|
||||
header, payload = _decode_jws(_value, key, algorithms, registry)
|
||||
|
||||
try:
|
||||
claims: Claims = json.loads(payload, cls=decoder_cls)
|
||||
except (TypeError, ValueError):
|
||||
raise InvalidPayloadError()
|
||||
|
||||
return Token(header, claims)
|
||||
|
||||
|
||||
def _decode_jwe(
|
||||
value: bytes, key: KeyFlexible, algorithms: Collection[str] | None = None, registry: JWERegistry | None = None
|
||||
) -> tuple[Header, bytes]:
|
||||
jwe_obj = decrypt_compact(value, key, algorithms, registry)
|
||||
assert jwe_obj.plaintext is not None
|
||||
return jwe_obj.headers(), jwe_obj.plaintext
|
||||
|
||||
|
||||
def _decode_jws(
|
||||
value: bytes, key: KeyFlexible, algorithms: Collection[str] | None = None, registry: JWSRegistry | None = None
|
||||
) -> tuple[Header, bytes]:
|
||||
jws_obj = deserialize_compact(value, key, algorithms, registry)
|
||||
assert jws_obj.payload is not None
|
||||
return jws_obj.headers(), jws_obj.payload
|
||||
@@ -0,0 +1,217 @@
|
||||
from typing import Any, Callable, Union
|
||||
from .errors import (
|
||||
MissingHeaderError,
|
||||
MissingCritHeaderError,
|
||||
UnsupportedHeaderError,
|
||||
InvalidHeaderValueError,
|
||||
)
|
||||
|
||||
Header = dict[str, Any]
|
||||
|
||||
|
||||
def is_str(value: Any) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("must be a str")
|
||||
|
||||
|
||||
def is_url(value: str) -> None:
|
||||
is_str(value)
|
||||
if not value.startswith(("http://", "https://")):
|
||||
raise ValueError("must be a URL")
|
||||
|
||||
|
||||
def is_int(value: int) -> None:
|
||||
if not isinstance(value, int):
|
||||
raise ValueError("must be an int")
|
||||
|
||||
|
||||
def is_bool(value: bool) -> None:
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("must be an bool")
|
||||
|
||||
|
||||
def is_list_str(values: list[str]) -> None:
|
||||
if not isinstance(values, list):
|
||||
raise ValueError("must be a list[str]")
|
||||
|
||||
if not all(isinstance(value, str) for value in values):
|
||||
raise ValueError("must be a list[str]")
|
||||
|
||||
|
||||
def is_jwk(value: dict[str, Any]) -> None:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("must be a JWK")
|
||||
|
||||
|
||||
def in_choices(choices: list[str]) -> Callable[[Union[str, list[str]]], None]:
|
||||
def _is_one_of(value: str | list[str]) -> None:
|
||||
if isinstance(value, list):
|
||||
if not all(v in choices for v in value):
|
||||
raise ValueError(f"must be one of {choices}")
|
||||
|
||||
elif value not in choices:
|
||||
raise ValueError(f"must be one of {choices}")
|
||||
|
||||
return _is_one_of
|
||||
|
||||
|
||||
def not_support(_: Any) -> None:
|
||||
raise ValueError("is not supported")
|
||||
|
||||
|
||||
Validate = Callable[[Any], None]
|
||||
_value_validators: dict[str, Validate] = {
|
||||
"str": is_str,
|
||||
"list[str]": is_list_str,
|
||||
"int": is_int,
|
||||
"bool": is_bool,
|
||||
"url": is_url,
|
||||
"jwk": is_jwk,
|
||||
"none": not_support,
|
||||
}
|
||||
|
||||
|
||||
class HeaderParameter:
|
||||
"""Define the header parameter for JWS and JWE."""
|
||||
|
||||
def __init__(self, description: str, validate: str | Validate, required: bool = False):
|
||||
#: a short description of the header parameter
|
||||
self.description = description
|
||||
#: a function for validating the header parameter's value
|
||||
self.validate = _value_validators[validate] if isinstance(validate, str) else validate
|
||||
#: if this header parameter is required
|
||||
self.required = required
|
||||
|
||||
|
||||
#: Define header parameters for JWS and JWE
|
||||
HeaderRegistryDict = dict[str, HeaderParameter]
|
||||
|
||||
|
||||
class KeyParameter:
|
||||
"""Define the key parameter for JWK."""
|
||||
|
||||
def __init__(self, description: str, validate: str | Validate, private: bool | None = None, required: bool = False):
|
||||
#: a short description of the key parameter
|
||||
self.description: str = description
|
||||
#: a function for validating the key parameter's value
|
||||
self.validate = _value_validators[validate] if isinstance(validate, str) else validate
|
||||
#: if this key parameter for private key only
|
||||
self.private = private
|
||||
#: if this key parameter is required
|
||||
self.required = required
|
||||
|
||||
|
||||
class KeyOperation:
|
||||
def __init__(self, description: str, use: str, private: bool | None):
|
||||
self.description = description
|
||||
self.use = use
|
||||
self.private = private
|
||||
|
||||
|
||||
#: Define parameters for JWK
|
||||
KeyParameterRegistryDict = dict[str, KeyParameter]
|
||||
KeyOperationRegistryDict = dict[str, KeyOperation]
|
||||
|
||||
#: Basic JWS header registry
|
||||
JWS_HEADER_REGISTRY: HeaderRegistryDict = {
|
||||
"alg": HeaderParameter("Algorithm", is_str, True),
|
||||
"jku": HeaderParameter("JWK Set URL", is_url),
|
||||
"jwk": HeaderParameter("JSON Web Key", is_jwk),
|
||||
"kid": HeaderParameter("Key ID", is_str),
|
||||
"x5u": HeaderParameter("X.509 URL", is_url),
|
||||
"x5c": HeaderParameter("X.509 Certificate Chain", is_list_str),
|
||||
"x5t": HeaderParameter("X.509 Certificate SHA-1 Thumbprint", is_str),
|
||||
"x5t#S256": HeaderParameter("X.509 Certificate SHA-256 Thumbprint", is_str),
|
||||
"typ": HeaderParameter("Type", is_str),
|
||||
"cty": HeaderParameter("Content Type", is_str),
|
||||
"crit": HeaderParameter("Critical", is_list_str),
|
||||
# Enable RFC7797 by default.
|
||||
"b64": HeaderParameter("JWS Signing Input Formula", is_bool),
|
||||
}
|
||||
|
||||
#: Basic JWE header registry
|
||||
JWE_HEADER_REGISTRY = {
|
||||
"enc": HeaderParameter("Encryption Algorithm", is_str, True),
|
||||
"zip": HeaderParameter("Compression Algorithm", is_str),
|
||||
**JWS_HEADER_REGISTRY,
|
||||
}
|
||||
|
||||
#: Basic JWK parameter registry
|
||||
JWK_PARAMETER_REGISTRY = {
|
||||
"kty": KeyParameter("Key Type", is_str, required=True), # This member MUST be present in a JWK.
|
||||
"use": KeyParameter("Public Key Use", in_choices(["sig", "enc"])),
|
||||
"key_ops": KeyParameter(
|
||||
"Key Operations",
|
||||
in_choices(
|
||||
[
|
||||
"sign",
|
||||
"verify",
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
"wrapKey",
|
||||
"unwrapKey",
|
||||
"deriveKey",
|
||||
"deriveBits",
|
||||
]
|
||||
),
|
||||
),
|
||||
"alg": KeyParameter("Algorithm", is_str),
|
||||
"kid": KeyParameter("Key ID", is_str),
|
||||
"x5u": KeyParameter("X.509 URL", is_url),
|
||||
"x5c": KeyParameter("X.509 Certificate Chain", is_list_str),
|
||||
"x5t": KeyParameter("X.509 Certificate SHA-1 Thumbprint", is_str),
|
||||
"x5t#S256": KeyParameter("X.509 Certificate SHA-256 Thumbprint", is_str),
|
||||
}
|
||||
|
||||
#: Common JWK operations
|
||||
#: https://www.rfc-editor.org/rfc/rfc7517#section-4.3
|
||||
JWK_OPERATION_REGISTRY = {
|
||||
"sign": KeyOperation("compute digital signature or MAC", "sig", True),
|
||||
"verify": KeyOperation("verify digital signature or MAC", "sig", False),
|
||||
"encrypt": KeyOperation("encrypt content", "enc", False),
|
||||
"decrypt": KeyOperation("decrypt content and validate decryption, if applicable", "enc", True),
|
||||
"wrapKey": KeyOperation("encrypt key", "enc", False),
|
||||
"unwrapKey": KeyOperation("decrypt key and validate decryption, if applicable", "enc", True),
|
||||
"deriveKey": KeyOperation("derive key", "enc", False),
|
||||
"deriveBits": KeyOperation("derive bits not to be used as a key", "enc", None),
|
||||
}
|
||||
|
||||
|
||||
def check_supported_header(registry: HeaderRegistryDict, header: Header) -> None:
|
||||
allowed_keys = set(registry.keys())
|
||||
unsupported_keys = set(header.keys()) - allowed_keys
|
||||
if unsupported_keys:
|
||||
raise UnsupportedHeaderError(f"Unsupported {unsupported_keys} in header")
|
||||
|
||||
|
||||
def check_registry_header(registry: HeaderRegistryDict, header: Header, check_required: bool = True) -> None:
|
||||
for key, reg in registry.items():
|
||||
if check_required and reg.required and key not in header:
|
||||
raise MissingHeaderError(key)
|
||||
if key in header:
|
||||
try:
|
||||
reg.validate(header[key])
|
||||
except ValueError as error:
|
||||
raise InvalidHeaderValueError(f"'{key}' in header {error}")
|
||||
|
||||
|
||||
def check_crit_header(registry: HeaderRegistryDict, header: Header) -> None:
|
||||
# check `crit` header
|
||||
missing_crit_headers = []
|
||||
unsupported_crit_headers = []
|
||||
if "crit" in header:
|
||||
for k in header["crit"]:
|
||||
if k not in header:
|
||||
missing_crit_headers.append(k)
|
||||
elif k not in registry:
|
||||
unsupported_crit_headers.append(k)
|
||||
|
||||
if missing_crit_headers:
|
||||
raise MissingCritHeaderError(",".join(missing_crit_headers))
|
||||
elif unsupported_crit_headers:
|
||||
raise UnsupportedHeaderError(f"Unsupported {unsupported_crit_headers} in header")
|
||||
|
||||
|
||||
def reject_unprotected_crit_header(unprotected: Header | None) -> None:
|
||||
if unprotected and "crit" in unprotected:
|
||||
raise UnsupportedHeaderError("'crit' header MUST be protected header")
|
||||
@@ -0,0 +1,69 @@
|
||||
from typing import Any
|
||||
import base64
|
||||
import struct
|
||||
import binascii
|
||||
import json
|
||||
|
||||
|
||||
def to_bytes(x: Any, charset: str = "utf-8", errors: str = "strict") -> bytes:
|
||||
if isinstance(x, bytes):
|
||||
return x
|
||||
if isinstance(x, str):
|
||||
return x.encode(charset, errors)
|
||||
if isinstance(x, (int, float)):
|
||||
return str(x).encode(charset, errors)
|
||||
return bytes(x)
|
||||
|
||||
|
||||
def to_str(x: bytes | str, charset: str = "utf-8") -> str:
|
||||
if isinstance(x, bytes):
|
||||
return x.decode(charset)
|
||||
return x
|
||||
|
||||
|
||||
def urlsafe_b64decode(s: bytes) -> bytes:
|
||||
if b"+" in s or b"/" in s or b"=" in s:
|
||||
raise binascii.Error
|
||||
|
||||
pad = -len(s) % 4
|
||||
if pad == 3:
|
||||
raise binascii.Error
|
||||
|
||||
safe_ending = (b"AEIMQUYcgkosw048", b"AQgw")
|
||||
if pad and s[-1] not in safe_ending[pad - 1]:
|
||||
raise binascii.Error
|
||||
|
||||
s += b"=" * pad
|
||||
return base64.b64decode(s, b"-_", validate=True)
|
||||
|
||||
|
||||
def urlsafe_b64encode(s: bytes) -> bytes:
|
||||
return base64.urlsafe_b64encode(s).rstrip(b"=")
|
||||
|
||||
|
||||
def base64_to_int(s: str) -> int:
|
||||
data = urlsafe_b64decode(to_bytes(s))
|
||||
buf = struct.unpack("%sB" % len(data), data)
|
||||
return int("".join(["%02x" % byte for byte in buf]), 16)
|
||||
|
||||
|
||||
def int_to_base64(num: int, byte_count: int | None = None) -> str:
|
||||
if num < 0:
|
||||
raise ValueError("Must be a positive integer")
|
||||
|
||||
if byte_count is None:
|
||||
byte_count = (num.bit_length() + 7) // 8
|
||||
elif num.bit_length() > byte_count * 8:
|
||||
raise ValueError("Number too large for byte count")
|
||||
|
||||
s = num.to_bytes(byte_count, "big", signed=False)
|
||||
return urlsafe_b64encode(s).decode("utf-8", "strict")
|
||||
|
||||
|
||||
def json_b64encode(data: dict[str, Any]) -> bytes:
|
||||
text = json.dumps(data, ensure_ascii=True, separators=(",", ":"))
|
||||
return urlsafe_b64encode(to_bytes(text, "ascii"))
|
||||
|
||||
|
||||
def json_b64decode(text: bytes) -> Any:
|
||||
return json.loads(urlsafe_b64decode(text))
|
||||
Reference in New Issue
Block a user