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,35 @@
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
from ..errors import InvalidKeyTypeError
from .._rfc7515.model import JWSAlgModel
from .okp_key import OKPKey
class EdDSAAlgorithm(JWSAlgModel):
name = "EdDSA"
description = "Edwards-curve Digital Signature Algorithm for JWS"
key_type = "OKP"
security_warning = "EdDSA is deprecated via RFC 9864"
def sign(self, msg: bytes, key: OKPKey) -> bytes:
op_key = key.get_op_key("sign")
if not isinstance(op_key, (Ed25519PrivateKey, Ed448PrivateKey)):
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' or 'Ed448' OKP key")
return op_key.sign(msg)
def verify(self, msg: bytes, sig: bytes, key: OKPKey) -> bool:
op_key = key.get_op_key("verify")
if not isinstance(op_key, (Ed25519PublicKey, Ed448PublicKey)):
raise InvalidKeyTypeError(f"Algorithm '{self.name}' requires 'Ed25519' or 'Ed448' OKP key")
try:
op_key.verify(sig, msg)
return True
except InvalidSignature:
return False
EdDSA = EdDSAAlgorithm()
# compatible
EdDSAAlgModel = EdDSAAlgorithm
@@ -0,0 +1,326 @@
import typing as t
from functools import cached_property
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey, Ed448PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey, X25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey, X448PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding,
PublicFormat,
PrivateFormat,
NoEncryption,
)
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from .._rfc7517.models import CurveKey
from .._rfc7517.types import KeyParameters, AnyKey
from .._rfc7517.pem import CryptographyBinding
from ..errors import InvalidExchangeKeyError, InvalidKeyCurveError
from ..util import to_bytes, urlsafe_b64decode, urlsafe_b64encode
from ..registry import KeyParameter
LiteralCurves = t.Literal["Ed25519", "Ed448", "X25519", "X448"]
PublicOKPKey = t.Union[Ed25519PublicKey, Ed448PublicKey, X25519PublicKey, X448PublicKey]
PrivateOKPKey = t.Union[Ed25519PrivateKey, Ed448PrivateKey, X25519PrivateKey, X448PrivateKey]
OKPDictKey = t.TypedDict(
"OKPDictKey",
{
"crv": LiteralCurves,
"x": str,
"d": str,
},
total=False,
)
PUBLIC_KEYS_MAP: dict[str, type[PublicOKPKey]] = {
"Ed25519": Ed25519PublicKey,
"Ed448": Ed448PublicKey,
"X25519": X25519PublicKey,
"X448": X448PublicKey,
}
PRIVATE_KEYS_MAP: dict[str, type[PrivateOKPKey]] = {
"Ed25519": Ed25519PrivateKey,
"Ed448": Ed448PrivateKey,
"X25519": X25519PrivateKey,
"X448": X448PrivateKey,
}
OKP_SEED_SIZES: dict[LiteralCurves, int] = {
"Ed25519": 32,
"Ed448": 57,
"X25519": 32,
"X448": 56,
}
PrivateKeyTypes = (Ed25519PrivateKey, Ed448PrivateKey, X25519PrivateKey, X448PrivateKey)
class OKPBinding(CryptographyBinding):
key_type = "OKP"
ssh_type = b"ssh-ed25519"
_cryptography_key_types = (
Ed25519PublicKey,
Ed25519PrivateKey,
Ed448PublicKey,
Ed448PrivateKey,
X25519PublicKey,
X25519PrivateKey,
X448PublicKey,
X448PrivateKey,
)
@staticmethod
def generate_private_key(crv: LiteralCurves) -> PrivateOKPKey:
if crv not in PRIVATE_KEYS_MAP:
raise InvalidKeyCurveError(f"Invalid curve value: '{crv}'")
crv_key: type[PrivateOKPKey] = PRIVATE_KEYS_MAP[crv]
return crv_key.generate()
@staticmethod
def from_private_bytes(crv: LiteralCurves, data: bytes) -> PrivateOKPKey:
crv_key: type[PrivateOKPKey] = PRIVATE_KEYS_MAP[crv]
return crv_key.from_private_bytes(data)
@staticmethod
def from_public_bytes(crv: LiteralCurves, data: bytes) -> PublicOKPKey:
crv_key: type[PublicOKPKey] = PUBLIC_KEYS_MAP[crv]
return crv_key.from_public_bytes(data)
@classmethod
def import_private_key(cls, obj: OKPDictKey) -> PrivateOKPKey:
d = urlsafe_b64decode(to_bytes(obj["d"]))
return cls.from_private_bytes(obj["crv"], d)
@classmethod
def import_public_key(cls, obj: OKPDictKey) -> PublicOKPKey:
x = urlsafe_b64decode(to_bytes(obj["x"]))
return cls.from_public_bytes(obj["crv"], x)
@classmethod
def export_private_key(cls, key: PrivateOKPKey) -> dict[str, str]:
obj = cls.export_public_key(key.public_key())
d_bytes = key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
obj["d"] = urlsafe_b64encode(d_bytes).decode("utf-8")
return obj
@classmethod
def export_public_key(cls, key: PublicOKPKey) -> dict[str, str]:
x_bytes = key.public_bytes(Encoding.Raw, PublicFormat.Raw)
return {
"crv": get_key_curve(key),
"x": urlsafe_b64encode(x_bytes).decode("utf-8"),
}
class OKPKey(CurveKey[PrivateOKPKey, PublicOKPKey]):
"""Key class of the ``OKP`` key type."""
key_type = "OKP"
#: Registry definition for OKP Key
#: https://www.rfc-editor.org/rfc/rfc8037#section-2
value_registry = {
"crv": KeyParameter("Curve", "str", private=False, required=True),
"x": KeyParameter("X Coordinate", "str", private=False, required=True),
"d": KeyParameter("OKP Private Key", "str", private=True, required=False),
}
binding = OKPBinding
def exchange_derive_key(self, key: "OKPKey") -> bytes:
# used in ECDH-ES Algorithms
pubkey = t.cast(t.Union[X25519PublicKey, X448PublicKey], key.get_op_key("deriveKey"))
# this if else logic is used for type hints
if isinstance(self.private_key, X25519PrivateKey) and isinstance(pubkey, X25519PublicKey):
return self.private_key.exchange(pubkey)
elif isinstance(self.private_key, X448PrivateKey) and isinstance(pubkey, X448PublicKey):
return self.private_key.exchange(pubkey)
raise InvalidExchangeKeyError()
@property
def is_private(self) -> bool:
return isinstance(self.raw_value, PrivateKeyTypes)
@cached_property
def public_key(self) -> PublicOKPKey:
if isinstance(self.raw_value, PrivateKeyTypes):
return self.raw_value.public_key()
return self.raw_value
@property
def private_key(self) -> PrivateOKPKey | None:
if isinstance(self.raw_value, PrivateKeyTypes):
return self.raw_value
return None
@property
def curve_name(self) -> LiteralCurves:
return get_key_curve(self.raw_value)
@classmethod
def import_key(
cls: t.Any,
value: AnyKey | PrivateOKPKey | PublicOKPKey,
parameters: KeyParameters | None = None,
password: t.Any = None,
) -> "OKPKey":
key: OKPKey
if isinstance(
value,
(
Ed25519PrivateKey,
Ed448PrivateKey,
X25519PrivateKey,
X448PrivateKey,
Ed25519PublicKey,
Ed448PublicKey,
X25519PublicKey,
X448PublicKey,
),
):
key = cls(value, value, parameters)
else:
key = super(OKPKey, cls).import_key(value, parameters, password)
return key
@classmethod
def generate_key(
cls: type["OKPKey"],
crv: LiteralCurves | None = "Ed25519",
parameters: KeyParameters | None = None,
private: bool = True,
auto_kid: bool = False,
) -> "OKPKey":
"""Generate a ``OKPKey`` with the given "crv" value.
:param crv: OKPKey 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:
raw_key = cls.binding.generate_private_key("Ed25519")
else:
raw_key = cls.binding.generate_private_key(crv)
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
@classmethod
def derive_key(
cls: type["OKPKey"],
secret: bytes | str,
crv: LiteralCurves = "Ed25519",
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,
) -> "OKPKey":
"""
Derives a key from a given input secret using a specified key derivation function
(KDF) and elliptic curve algorithm.
To derive a key using **HKDF**, the ``kdf_options`` may contain the ``algorithm``,
``salt`` and ``info`` values:
.. code-block:: python
from cryptography.hazmat.primitives import hashes
from joserfc.jwk import OKPKey
# default kdf_name is HKDF, algorithm is SHA256
OKPKey.derive_key("secret")
# equivalent to
OKPKey.derive_key(
"secret", "Ed25519",
kdf_name="HKDF",
kdf_options={
"algorithm": hashes.SHA256(),
"salt": b"joserfc:OKP:HKDF:Ed25519",
"info": b"",
}
)
To derive a key using **PBKDF2**, the ``kdf_options`` may contain the ``algorithm``,
``salt`` and ``iterations`` values:
.. code-block:: python
from cryptography.hazmat.primitives import hashes
from joserfc.jwk import OKPKey
OKPKey.derive_key("secret", kdf_name="PBKDF2")
# equivalent to
OKPKey.derive_key(
"secret", "Ed25519",
kdf_name="PBKDF2",
kdf_options={
"algorithm": hashes.SHA256(),
"salt": b"joserfc:OKP:PBKDF2:Ed25519",
"iterations": 100000,
}
)
:param secret: The input secret used for key derivation
:param crv: OKPKey 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
"""
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:OKP:{kdf_name}:{crv}"))
if kdf_name == "HKDF":
kdf_options.setdefault("info", b"")
hkdf = HKDF(
algorithm=algorithm,
length=OKP_SEED_SIZES[crv],
**kdf_options,
)
seed = hkdf.derive(to_bytes(secret))
elif kdf_name == "PBKDF2":
kdf_options.setdefault("iterations", 100000)
pbkdf2 = PBKDF2HMAC(
algorithm=algorithm,
length=OKP_SEED_SIZES[crv],
**kdf_options,
)
seed = pbkdf2.derive(to_bytes(secret))
else:
raise ValueError(f"Invalid kdf value: '{kdf_name}'")
raw_key = cls.binding.from_private_bytes(crv, seed)
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
def get_key_curve(key: t.Union[PublicOKPKey, PrivateOKPKey]) -> LiteralCurves:
if isinstance(key, (Ed25519PublicKey, Ed25519PrivateKey)):
return "Ed25519"
elif isinstance(key, (Ed448PublicKey, Ed448PrivateKey)):
return "Ed448"
elif isinstance(key, (X25519PublicKey, X25519PrivateKey)):
return "X25519"
elif isinstance(key, (X448PublicKey, X448PrivateKey)):
return "X448"
raise ValueError("Invalid key") # pragma: no cover
def _wrap_key(
cls: type[OKPKey],
raw_key: PrivateOKPKey,
private: bool,
auto_kid: bool,
parameters: KeyParameters | None = None,
) -> OKPKey:
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