354 lines
11 KiB
Python
354 lines
11 KiB
Python
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)
|