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,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