202 lines
6.8 KiB
Python
202 lines
6.8 KiB
Python
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
|