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