Root cause
Validation & fail-open logic introduced
The Claude-generated module decoded a key bundle and exposed to_identity() while leaving signature verification as an optional caller responsibility.
How AI contributed: Direct introductionPublished Apr 1, 2026 in
jahlives/openssl_encryptAll 20 findings in this repository
PublicKeyBundle.from_dict() decoded caller-controlled keys and a self-signature but returned the bundle without checking that signature. to_identity() then turned the unverified keys into a usable recipient identity. Version 1.4.0 verifies the signature before deserialization returns.
Root cause
The Claude-generated module decoded a key bundle and exposed to_identity() while leaving signature verification as an optional caller responsibility.
Fix
Version 1.4.0 verifies the self-signature inside from_dict() by default and rejects invalid bundles before they can become recipient identities.
Case facts
@@ -0,0 +328,34 @@ selected from new openssl_encrypt/modules/key_bundle.py+ @classmethod+ def from_dict(cls, data: Dict) -> "PublicKeyBundle":+ """+ Create bundle from dictionary.++ SECURITY: Does NOT verify signature. Call verify_signature() after creation.++ Args:+ data: Dictionary from to_dict()++ Returns:+ PublicKeyBundle (unverified)++ Raises:+ KeyError: If required fields missing+ ValueError: If data format invalid+ """+ try:+ bundle = cls(+ name=data["name"],+ email=data.get("email"),+ fingerprint=data["fingerprint"],+ created_at=data["created_at"],+ encryption_public_key=base64.b64decode(data["encryption_public_key"]),+ signing_public_key=base64.b64decode(data["signing_public_key"]),+ encryption_algorithm=data["encryption_algorithm"],+ signing_algorithm=data["signing_algorithm"],+ self_signature=base64.b64decode(data["self_signature"]),+ )+ return bundle+ except KeyError as e:+ raise ValueError(f"Missing required field in bundle data: {e}")+ except Exception as e:+ raise ValueError(f"Invalid bundle data format: {e}")@@ -0,0 +363,29 @@ selected from new openssl_encrypt/modules/key_bundle.py+ def to_identity(self) -> "Identity":+ """+ Convert bundle to Identity (public keys only).++ SECURITY: Creates Identity WITHOUT private keys (is_own_identity=False).+ This is safe for identities received from keyserver.++ Returns:+ Identity instance (public keys only)+ """+ # Import Identity here to avoid circular import+ from .identity import Identity++ identity = Identity(+ name=self.name,+ email=self.email,+ fingerprint=self.fingerprint,+ created_at=self.created_at,+ encryption_algorithm=self.encryption_algorithm,+ signing_algorithm=self.signing_algorithm,+ encryption_public_key=self.encryption_public_key,+ encryption_private_key=None,+ signing_public_key=self.signing_public_key,+ signing_private_key=None,+ is_own_identity=False,+ )++ logger.debug(f"Converted bundle to Identity for '{self.name}'")+ return identity@@ -326,21 +326,21 @@ class PublicKeyBundle: } @classmethod- def from_dict(cls, data: Dict) -> "PublicKeyBundle":+ def from_dict(cls, data: Dict, verify: bool = True) -> "PublicKeyBundle": """ Create bundle from dictionary. - SECURITY: Does NOT verify signature. Call verify_signature() after creation.- Args: data: Dictionary from to_dict()+ verify: If True (default), verify self_signature before returning.+ Set to False only if verification happens separately. Returns:- PublicKeyBundle (unverified)+ PublicKeyBundle (verified if verify=True) Raises: KeyError: If required fields missing- ValueError: If data format invalid+ ValueError: If data format invalid or signature verification fails """ try: bundle = cls(@@ -354,9 +354,16 @@ class PublicKeyBundle: signing_algorithm=data["signing_algorithm"], self_signature=base64.b64decode(data["self_signature"]), )+ if verify:+ try:+ bundle.verify_signature()+ except Exception as e:+ raise ValueError(f"Bundle signature verification failed: {e}") return bundle except KeyError as e: raise ValueError(f"Missing required field in bundle data: {e}")+ except ValueError:+ raise except Exception as e: raise ValueError(f"Invalid bundle data format: {e}") AI-assisted change 301c66ad900d9b5a2591f71192abc97b788e7cfee070386582d2f3361dcc34d3 · Fix ec479da1229a4e27b1f1667c9c07eef0b775aa36fdcb571d01d9decdc34fa15c