import typing as t from typing import Any import secrets import warnings from ..errors import SecurityWarning from ..util import ( to_bytes, urlsafe_b64decode, urlsafe_b64encode, ) from ..registry import KeyParameter from .._rfc7517.models import SymmetricKey, NativeKeyBinding from .._rfc7517.types import KeyParameters, DictKey, AnyKey POSSIBLE_UNSAFE_KEYS = ( b"-----BEGIN ", b"---- BEGIN ", b"ssh-rsa ", b"ssh-dss ", b"ssh-ed25519 ", b"ecdsa-sha2-", ) class OctBinding(NativeKeyBinding): @classmethod def convert_raw_key_to_dict(cls, raw_key: bytes, private: bool) -> DictKey: k = urlsafe_b64encode(raw_key).decode("utf-8") return {"k": k} @classmethod def import_from_dict(cls, value: DictKey) -> bytes: return urlsafe_b64decode(to_bytes(value["k"])) @classmethod def import_from_bytes(cls, value: bytes, password: Any | None = None) -> bytes: # security check if value.startswith(POSSIBLE_UNSAFE_KEYS): warnings.warn("This key should not be used as an oct key", SecurityWarning) return value class OctKey(SymmetricKey): """OctKey is a symmetric key, defined by RFC7518 Section 6.4.""" key_type = "oct" binding = OctBinding #: https://www.rfc-editor.org/rfc/rfc7518#section-6.4 value_registry = {"k": KeyParameter("Key Value", "str", True, True)} @classmethod def import_key( cls: Any, value: AnyKey, parameters: KeyParameters | None = None, password: Any = None, ) -> "OctKey": key: OctKey = super(OctKey, cls).import_key(value, parameters, password) if not key.raw_value: # An empty oct key material produces a deterministic HMAC digest # that any party can reproduce, allowing trivial signature forgery # in JWS HS256/HS384/HS512 verification. Reject outright rather # than emitting a SecurityWarning that callers commonly suppress. raise ValueError("oct key material must not be empty") if len(key.raw_value) < 14: # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final warnings.warn("Key size should be >= 112 bits", SecurityWarning) return key @classmethod def generate_key( cls, key_size: int | None = 256, parameters: KeyParameters | None = None, private: bool = True, auto_kid: bool = False, ) -> "OctKey": """Generate a ``OctKey`` with the given bit size (not bytes). :param key_size: size in bit :param parameters: extra parameter in JWK :param private: must be True :param auto_kid: add ``kid`` automatically """ if not private: raise ValueError("oct key can not be generated as public") if key_size is None: key_size = 256 if key_size % 8 != 0: raise ValueError("Invalid bit size for oct key") if key_size < 112: # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final warnings.warn("Key size should be >= 112 bits", SecurityWarning) raw_key = secrets.token_bytes(key_size // 8) key: OctKey = cls(raw_key, raw_key, parameters) if auto_kid: key.ensure_kid() return key def as_dict(self, private: bool = False, **params: t.Any) -> DictKey: return super().as_dict(private=True, **params)