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