first commit

This commit is contained in:
linkitapps
2026-09-14 23:22:10 +09:00
commit 9f9c8fb69b
4207 changed files with 1333187 additions and 0 deletions
+217
View File
@@ -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")