"""KeysAndCert — immutable container combining public key, signing key, and certificate. Ported from net.i2p.data.KeysAndCert. Serialized form (387+ bytes): - 256 bytes: public key area (ElGamal key or padding + shorter key) - 128 bytes: signing key area (DSA key or padding + shorter key) - 3+ bytes: certificate With KEY certificate and shorter keys (e.g., X25519 + EdDSA), the key data is right-aligned in each area, and the leading padding bytes are included in the certificate as extra key data. """ from __future__ import annotations import hashlib class KeysAndCert: """Immutable container for public key + signing public key + certificate. This is the base class for Destination and RouterIdentity. """ __slots__ = ("_public_key", "_signing_public_key", "_certificate", "_cached_hash", "_raw") # Standard area sizes in serialized form PUBKEY_AREA_SIZE = 256 SIGKEY_AREA_SIZE = 128 def __init__(self, public_key, signing_public_key, certificate, raw: bytes | None = None) -> None: self._public_key = public_key self._signing_public_key = signing_public_key self._certificate = certificate self._raw = raw self._cached_hash: bytes | None = None @property def public_key(self): return self._public_key @property def signing_public_key(self): return self._signing_public_key @property def certificate(self): return self._certificate def to_bytes(self) -> bytes: """Serialize to wire format: 256 + 128 + cert bytes.""" if self._raw is not None: return self._raw from i2p_data.key_types import PublicKey, SigningPublicKey from i2p_data.certificate import CertificateType pub_data = self._public_key.to_bytes() sig_data = self._signing_public_key.to_bytes() # Pad public key area to 256 bytes (right-aligned) if len(pub_data) < self.PUBKEY_AREA_SIZE: pub_area = b"\x00" * (self.PUBKEY_AREA_SIZE - len(pub_data)) + pub_data else: pub_area = pub_data[:self.PUBKEY_AREA_SIZE] # Pad signing key area to 128 bytes (right-aligned) if len(sig_data) < self.SIGKEY_AREA_SIZE: sig_area = b"\x00" * (self.SIGKEY_AREA_SIZE - len(sig_data)) + sig_data else: sig_area = sig_data[:self.SIGKEY_AREA_SIZE] cert_data = self._certificate.to_bytes() return pub_area + sig_area + cert_data @classmethod def from_bytes(cls, data: bytes) -> "KeysAndCert": """Deserialize from wire format.""" import io from i2p_data.key_types import PublicKey, SigningPublicKey, EncType from i2p_data.certificate import Certificate, CertificateType, KeyCertificate from i2p_crypto.dsa import SigType if len(data) < 387: raise ValueError(f"KeysAndCert requires at least 387 bytes, got {len(data)}") pub_area = data[:cls.PUBKEY_AREA_SIZE] sig_area = data[cls.PUBKEY_AREA_SIZE:cls.PUBKEY_AREA_SIZE + cls.SIGKEY_AREA_SIZE] cert_data = data[cls.PUBKEY_AREA_SIZE + cls.SIGKEY_AREA_SIZE:] # Parse certificate cert = Certificate.from_bytes(cert_data) # Determine key types from certificate if isinstance(cert, KeyCertificate): enc_type = cert.get_enc_type() or EncType.ELGAMAL sig_type = cert.get_sig_type() or SigType.DSA_SHA1 else: enc_type = EncType.ELGAMAL sig_type = SigType.DSA_SHA1 # Extract actual key data (right-aligned in area) pub_len = enc_type.pubkey_len pub_key_data = pub_area[cls.PUBKEY_AREA_SIZE - pub_len:] pub_key = PublicKey(pub_key_data, enc_type) sig_len = sig_type.pubkey_len sig_key_data = sig_area[cls.SIGKEY_AREA_SIZE - sig_len:] sig_key = SigningPublicKey(sig_key_data, sig_type) raw = data[:cls.PUBKEY_AREA_SIZE + cls.SIGKEY_AREA_SIZE + len(cert)] return cls(pub_key, sig_key, cert, raw=raw) def hash(self) -> bytes: """SHA-256 hash of the serialized form (cached).""" if self._cached_hash is None: self._cached_hash = hashlib.sha256(self.to_bytes()).digest() return self._cached_hash def __eq__(self, other: object) -> bool: if not isinstance(other, KeysAndCert): return NotImplemented return self.to_bytes() == other.to_bytes() def __hash__(self) -> int: return int.from_bytes(self.hash()[:4], "big") def __repr__(self) -> str: return f"{self.__class__.__name__}(hash={self.hash()[:4].hex()}...)"