first commit

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