first commit
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
import typing as t
|
||||
from collections.abc import KeysView
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from .types import DictKey, AnyKey, KeyParameters
|
||||
from .._rfc7638 import calculate_thumbprint
|
||||
from .._rfc9278 import concat_thumbprint_uri
|
||||
from ..registry import (
|
||||
KeyParameterRegistryDict,
|
||||
JWK_PARAMETER_REGISTRY,
|
||||
KeyOperationRegistryDict,
|
||||
JWK_OPERATION_REGISTRY,
|
||||
)
|
||||
from ..util import to_bytes
|
||||
from ..errors import (
|
||||
KeyParameterError,
|
||||
UnsupportedKeyUseError,
|
||||
UnsupportedKeyAlgorithmError,
|
||||
UnsupportedKeyOperationError,
|
||||
)
|
||||
|
||||
|
||||
NativePrivateKey = t.TypeVar("NativePrivateKey")
|
||||
NativePublicKey = t.TypeVar("NativePublicKey")
|
||||
GenericKey = t.TypeVar("GenericKey", bound="BaseKey[t.Any, t.Any]")
|
||||
|
||||
|
||||
class NativeKeyBinding(metaclass=ABCMeta):
|
||||
use_key_ops_registry: t.ClassVar[dict[str, list[str]]] = {
|
||||
"sig": ["sign", "verify"],
|
||||
"enc": ["encrypt", "decrypt", "wrapKey", "unwrapKey", "deriveKey", "deriveBits"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def convert_raw_key_to_dict(cls, raw_key: t.Any, private: bool) -> DictKey:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_from_dict(cls, value: DictKey) -> t.Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_from_bytes(cls, value: bytes, password: t.Any = None) -> t.Any:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def as_bytes(
|
||||
key: GenericKey,
|
||||
encoding: t.Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: str | None = None,
|
||||
) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key_registry(cls, dict_key: DictKey, registry: KeyParameterRegistryDict) -> None:
|
||||
for k in registry:
|
||||
if registry[k].required and k not in dict_key:
|
||||
raise KeyParameterError(f"'{k}' is required")
|
||||
|
||||
if k in dict_key:
|
||||
try:
|
||||
registry[k].validate(dict_key[k])
|
||||
except ValueError as error:
|
||||
raise KeyParameterError(f"'{k}' {error}")
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key_use_operations(cls, dict_key: DictKey) -> None:
|
||||
if "use" in dict_key and "key_ops" in dict_key:
|
||||
_use: str = dict_key["use"] # type: ignore
|
||||
operations = cls.use_key_ops_registry[_use]
|
||||
for op in dict_key["key_ops"]:
|
||||
if op not in operations:
|
||||
raise KeyParameterError("'use' and 'key_ops' does not match")
|
||||
|
||||
|
||||
class BaseKey(t.Generic[NativePrivateKey, NativePublicKey], metaclass=ABCMeta):
|
||||
key_type: t.ClassVar[str]
|
||||
binding: t.ClassVar[type[NativeKeyBinding]]
|
||||
value_registry: t.ClassVar[KeyParameterRegistryDict]
|
||||
param_registry: t.ClassVar[KeyParameterRegistryDict] = JWK_PARAMETER_REGISTRY
|
||||
operation_registry: t.ClassVar[KeyOperationRegistryDict] = JWK_OPERATION_REGISTRY
|
||||
thumbprint_digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_value: NativePrivateKey | NativePublicKey,
|
||||
original_value: t.Any,
|
||||
parameters: KeyParameters | None = None,
|
||||
):
|
||||
self._raw_value = raw_value
|
||||
self.original_value = original_value
|
||||
self.extra_parameters = parameters
|
||||
self._dict_value: DictKey = {}
|
||||
if isinstance(original_value, dict):
|
||||
if parameters is not None:
|
||||
data = {**original_value, **parameters, "kty": self.key_type}
|
||||
else:
|
||||
data = {**original_value, "kty": self.key_type}
|
||||
self.validate_dict_key(data)
|
||||
self._dict_value = data
|
||||
|
||||
def __eq__(self, other: t.Any) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.dict_value == other.dict_value
|
||||
|
||||
def keys(self) -> KeysView[str]:
|
||||
return self.dict_value.keys()
|
||||
|
||||
def __getitem__(self, k: str) -> str | list[str]:
|
||||
return self.dict_value[k]
|
||||
|
||||
def get(self, k: str, default: str | list[str] | None = None) -> str | list[str] | None:
|
||||
return self.dict_value.get(k, default)
|
||||
|
||||
def ensure_kid(self) -> None:
|
||||
"""Ensure this key has a ``kid``. If ``kid`` is not provided by default,
|
||||
it will generate the kid with ``.thumbprint`` method, which is defined
|
||||
by RFC7638."""
|
||||
if "kid" not in self.dict_value:
|
||||
self._dict_value["kid"] = self.thumbprint()
|
||||
|
||||
@property
|
||||
def kid(self) -> str | None:
|
||||
"""The "kid" value of the JSON Web Key."""
|
||||
return t.cast(t.Optional[str], self.get("kid"))
|
||||
|
||||
@property
|
||||
def alg(self) -> str | None:
|
||||
"""The "alg" value of the JSON Web Key."""
|
||||
return t.cast(t.Optional[str], self.get("alg"))
|
||||
|
||||
@property
|
||||
def raw_value(self) -> t.Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def dict_value(self) -> DictKey:
|
||||
"""Property of the Key in Dict (JSON)."""
|
||||
if self._dict_value:
|
||||
return self._dict_value
|
||||
|
||||
data = self.binding.convert_raw_key_to_dict(self.raw_value, self.is_private)
|
||||
if self.extra_parameters is not None:
|
||||
data.update(self.extra_parameters) # type: ignore
|
||||
data["kty"] = self.key_type
|
||||
self.validate_dict_key(data)
|
||||
self._dict_value = data
|
||||
return data
|
||||
|
||||
@property
|
||||
def public_key(self) -> NativePublicKey:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def private_key(self) -> NativePrivateKey | None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def thumbprint(self) -> str:
|
||||
"""Call this method will generate the thumbprint with algorithm
|
||||
defined in RFC7638."""
|
||||
fields = [k for k in self.value_registry if self.value_registry[k].required]
|
||||
fields.append("kty")
|
||||
data = {key: self.dict_value[key] for key in fields}
|
||||
return calculate_thumbprint(data, self.thumbprint_digest_method)
|
||||
|
||||
def thumbprint_uri(self) -> str:
|
||||
"""Call this method will generate the thumbprint URI
|
||||
defined in RFC9278."""
|
||||
value = self.thumbprint()
|
||||
return concat_thumbprint_uri(value, self.thumbprint_digest_method)
|
||||
|
||||
def as_dict(self, private: bool = False, **params: t.Any) -> DictKey:
|
||||
"""Output this key to a JWK format (in dict). By default, it will return
|
||||
the ``dict_value`` of this key.
|
||||
|
||||
:param private: determine whether this method should output private key or not
|
||||
:param params: other parameters added into this key
|
||||
:raise: ValueError
|
||||
"""
|
||||
# check private conflicts
|
||||
if private and not self.is_private:
|
||||
raise ValueError("This key is not a private key.")
|
||||
|
||||
data = self.dict_value.copy()
|
||||
if private:
|
||||
data.update(params)
|
||||
return data
|
||||
|
||||
# clear private fields
|
||||
for k in self.dict_value:
|
||||
if k in self.value_registry and self.value_registry[k].private:
|
||||
del data[k]
|
||||
|
||||
data.update(params)
|
||||
return data
|
||||
|
||||
def check_use(self, use: str) -> None:
|
||||
"""Check if this key supports the given "use".
|
||||
|
||||
Values defined by this specification are:
|
||||
|
||||
- "sig" (signature)
|
||||
- "enc" (encryption)
|
||||
|
||||
Other values MAY be used. The "use" value is a case-sensitive
|
||||
string. Use of the "use" member is OPTIONAL, unless the application
|
||||
requires its presence.
|
||||
|
||||
:param use: this key is used for, e.g. "sig", "enc"
|
||||
:raise UnsupportedKeyUseError: if this key is not designed for the given use
|
||||
"""
|
||||
designed_use = self.get("use")
|
||||
if designed_use and designed_use != use:
|
||||
raise UnsupportedKeyUseError(f"This key is designed to be used for '{designed_use}'")
|
||||
|
||||
def check_alg(self, alg: str) -> None:
|
||||
"""Check if this key supports the given "alg".
|
||||
|
||||
:param alg: the algorithm this key is intended to be used, e.g. "HS256", "ECDH-EC"
|
||||
:raise UnsupportedKeyAlgorithmError: if this key is not designed for the given algorithm
|
||||
"""
|
||||
designed_alg = self.get("alg")
|
||||
if designed_alg and designed_alg != alg:
|
||||
raise UnsupportedKeyAlgorithmError(f"This key is designed for algorithm '{designed_alg}'")
|
||||
|
||||
def check_key_op(self, operation: str) -> None:
|
||||
"""Check if the given key_op is supported by this key.
|
||||
|
||||
:param operation: key operation value, such as "sign", "encrypt".
|
||||
:raise UnsupportedKeyOperationError: if the operation is not supported by this key.
|
||||
"""
|
||||
key_ops = self.get("key_ops")
|
||||
if key_ops is not None and operation not in key_ops:
|
||||
raise UnsupportedKeyOperationError(f"Unsupported key_op '{operation}'")
|
||||
|
||||
assert operation in self.operation_registry
|
||||
reg = self.operation_registry[operation]
|
||||
if reg.private and not self.is_private:
|
||||
raise UnsupportedKeyOperationError(f"Invalid key_op '{operation}' for public key")
|
||||
|
||||
@t.overload
|
||||
def get_op_key(self, operation: t.Literal["verify", "encrypt", "wrapKey", "deriveKey"]) -> NativePublicKey: ...
|
||||
|
||||
@t.overload
|
||||
def get_op_key(self, operation: t.Literal["sign", "decrypt", "unwrapKey"]) -> NativePrivateKey: ...
|
||||
|
||||
def get_op_key(self, operation: str) -> NativePublicKey | NativePrivateKey:
|
||||
self.check_key_op(operation)
|
||||
reg = self.operation_registry[operation]
|
||||
if reg.private:
|
||||
assert self.private_key is not None
|
||||
return self.private_key
|
||||
return self.public_key
|
||||
|
||||
@classmethod
|
||||
def validate_dict_key(cls, data: DictKey) -> None:
|
||||
cls.binding.validate_dict_key_registry(data, cls.param_registry)
|
||||
cls.binding.validate_dict_key_registry(data, cls.value_registry)
|
||||
cls.binding.validate_dict_key_use_operations(data)
|
||||
|
||||
@classmethod
|
||||
def import_key(
|
||||
cls: type[GenericKey],
|
||||
value: AnyKey,
|
||||
parameters: KeyParameters | None = None,
|
||||
password: t.Any = None,
|
||||
) -> GenericKey:
|
||||
if isinstance(value, dict):
|
||||
cls.validate_dict_key(value)
|
||||
raw_key = cls.binding.import_from_dict(value)
|
||||
return cls(raw_key, value, parameters)
|
||||
|
||||
raw_key = cls.binding.import_from_bytes(to_bytes(value), password)
|
||||
return cls(raw_key, value, parameters)
|
||||
|
||||
@classmethod
|
||||
def generate_key(
|
||||
cls: type[GenericKey],
|
||||
*,
|
||||
parameters: KeyParameters | None = None,
|
||||
private: bool = True,
|
||||
auto_kid: bool = False,
|
||||
) -> GenericKey:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SymmetricKey(BaseKey[bytes, bytes], metaclass=ABCMeta):
|
||||
@property
|
||||
def raw_value(self) -> bytes:
|
||||
"""The raw key in bytes."""
|
||||
return self._raw_value
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
"""A symmetric key will always be private."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def public_key(self) -> bytes:
|
||||
"""Returns the ``raw_value`` as the public key."""
|
||||
return self.raw_value
|
||||
|
||||
@property
|
||||
def private_key(self) -> bytes:
|
||||
"""Returns the ``raw_value`` as the private key."""
|
||||
return self.raw_value
|
||||
|
||||
|
||||
class AsymmetricKey(BaseKey[NativePrivateKey, NativePublicKey], metaclass=ABCMeta):
|
||||
@property
|
||||
def raw_value(self) -> t.Union[NativePublicKey, NativePrivateKey]:
|
||||
return self._raw_value
|
||||
|
||||
def as_bytes(
|
||||
self,
|
||||
encoding: t.Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: str | None = None,
|
||||
) -> bytes:
|
||||
return self.binding.as_bytes(self, encoding, private, password)
|
||||
|
||||
def as_pem(self, private: bool = False, password: str | None = None) -> bytes:
|
||||
return self.as_bytes(private=private, password=password)
|
||||
|
||||
def as_der(self, private: bool = False, password: str | None = None) -> bytes:
|
||||
return self.as_bytes(encoding="DER", private=private, password=password)
|
||||
|
||||
|
||||
class CurveKey(AsymmetricKey[NativePrivateKey, NativePublicKey]):
|
||||
@property
|
||||
@abstractmethod
|
||||
def curve_name(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exchange_derive_key(self, key: t.Any) -> bytes:
|
||||
pass
|
||||
@@ -0,0 +1,163 @@
|
||||
from typing import Any, Literal, cast
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
load_ssh_public_key,
|
||||
load_ssh_private_key,
|
||||
load_der_private_key,
|
||||
load_der_public_key,
|
||||
Encoding,
|
||||
PrivateFormat,
|
||||
PublicFormat,
|
||||
KeySerializationEncryption,
|
||||
BestAvailableEncryption,
|
||||
NoEncryption,
|
||||
)
|
||||
from .models import NativeKeyBinding, GenericKey
|
||||
from .types import DictKey
|
||||
from ..errors import InvalidKeyTypeError
|
||||
from ..util import to_bytes
|
||||
|
||||
|
||||
def import_from_ssh_key(raw: bytes) -> Any:
|
||||
return load_ssh_public_key(raw)
|
||||
|
||||
|
||||
def import_from_pem_key(raw: bytes, password: bytes | None = None) -> Any:
|
||||
key: Any
|
||||
|
||||
if b"OPENSSH PRIVATE" in raw:
|
||||
key = load_ssh_private_key(raw, password=password)
|
||||
|
||||
elif b"PUBLIC" in raw:
|
||||
key = load_pem_public_key(raw)
|
||||
|
||||
elif b"PRIVATE" in raw:
|
||||
key = load_pem_private_key(raw, password=password)
|
||||
|
||||
elif b"CERTIFICATE" in raw:
|
||||
cert = load_pem_x509_certificate(raw)
|
||||
return cert.public_key()
|
||||
|
||||
else:
|
||||
try:
|
||||
key = load_der_private_key(raw, password=password)
|
||||
except ValueError:
|
||||
key = load_der_public_key(raw)
|
||||
return key
|
||||
|
||||
|
||||
def dump_pem_key(
|
||||
key: Any,
|
||||
encoding: Literal["PEM", "DER"] | None = None,
|
||||
private: bool | None = False,
|
||||
password: Any | None = None,
|
||||
) -> bytes:
|
||||
"""Export key into PEM/DER format bytes.
|
||||
|
||||
:param key: native cryptography key
|
||||
:param encoding: "PEM" or "DER"
|
||||
:param private: export private key or public key
|
||||
:param password: encrypt private key with password
|
||||
:return: bytes
|
||||
"""
|
||||
|
||||
if encoding is None or encoding == "PEM":
|
||||
encoding_enum = Encoding.PEM
|
||||
elif encoding == "DER":
|
||||
encoding_enum = Encoding.DER
|
||||
else: # pragma: no cover
|
||||
raise ValueError(f"Invalid encoding: {encoding}")
|
||||
|
||||
if private:
|
||||
encryption_algorithm: KeySerializationEncryption
|
||||
if password is None:
|
||||
encryption_algorithm = NoEncryption()
|
||||
else:
|
||||
encryption_algorithm = BestAvailableEncryption(to_bytes(password))
|
||||
value = key.private_bytes(
|
||||
encoding=encoding_enum,
|
||||
format=PrivateFormat.PKCS8,
|
||||
encryption_algorithm=encryption_algorithm,
|
||||
)
|
||||
else:
|
||||
value = key.public_bytes(
|
||||
encoding=encoding_enum,
|
||||
format=PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return cast(bytes, value)
|
||||
|
||||
|
||||
class CryptographyBinding(NativeKeyBinding, metaclass=ABCMeta):
|
||||
key_type: str
|
||||
ssh_type: bytes
|
||||
_cryptography_key_types: Any
|
||||
|
||||
@classmethod
|
||||
def check_ssh_type(cls, value: bytes) -> bool:
|
||||
return value.startswith(cls.ssh_type)
|
||||
|
||||
@classmethod
|
||||
def check_cryptography_key(cls, native_key: Any) -> bool:
|
||||
return isinstance(native_key, cls._cryptography_key_types)
|
||||
|
||||
@classmethod
|
||||
def convert_raw_key_to_dict(cls, raw_key: Any, private: bool) -> DictKey:
|
||||
if private:
|
||||
value = cls.export_private_key(raw_key)
|
||||
else:
|
||||
value = cls.export_public_key(raw_key)
|
||||
return cast(DictKey, value)
|
||||
|
||||
@classmethod
|
||||
def import_from_dict(cls, value: DictKey) -> Any:
|
||||
if "d" in value:
|
||||
return cls.import_private_key(value)
|
||||
return cls.import_public_key(value)
|
||||
|
||||
@classmethod
|
||||
def import_from_bytes(cls, value: bytes, password: Any | None = None) -> Any:
|
||||
if cls.check_ssh_type(value):
|
||||
return import_from_ssh_key(value)
|
||||
|
||||
if password is not None:
|
||||
password = to_bytes(password)
|
||||
|
||||
key = import_from_pem_key(value, password)
|
||||
if not cls.check_cryptography_key(key):
|
||||
raise InvalidKeyTypeError(f"Not a key of: '{cls.key_type}'")
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def as_bytes(
|
||||
key: GenericKey,
|
||||
encoding: Literal["PEM", "DER"] | None = None,
|
||||
private: bool = False,
|
||||
password: Any | None = None,
|
||||
) -> bytes:
|
||||
if private:
|
||||
return dump_pem_key(key.private_key, encoding, private, password)
|
||||
else:
|
||||
return dump_pem_key(key.public_key, encoding, private, password)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_private_key(cls, obj: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_public_key(cls, obj: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def export_private_key(cls, key: Any) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def export_public_key(cls, key: Any) -> Any:
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
import typing as t
|
||||
|
||||
__all__ = ["DictKey", "AnyKey", "KeyParameters"]
|
||||
|
||||
#: JSON Web Key in dict
|
||||
DictKey = dict[str, t.Union[str, list[str]]]
|
||||
|
||||
#: Key in str, bytes and dict
|
||||
AnyKey = t.Union[str, bytes, DictKey]
|
||||
|
||||
#: extra key parameters for JWK
|
||||
KeyParameters = t.TypedDict(
|
||||
"KeyParameters",
|
||||
{
|
||||
"use": str,
|
||||
"key_ops": list[str],
|
||||
"alg": str,
|
||||
"kid": str,
|
||||
"x5u": str,
|
||||
"x5c": list[str],
|
||||
"x5t": str,
|
||||
"x5t#S256": str,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
Reference in New Issue
Block a user