From c035a00815b5f5a79517ec3386d4cfd124733537 Mon Sep 17 00:00:00 2001 From: Andrew Engelbrecht Date: Sun, 16 Aug 2015 00:34:53 -0400 Subject: [PATCH] check to see if a reply encryption key can encrypt sometimes keys are revoked or expire. if so, they should not be encrypted to. edward now checks to make sure a key is good before encrypting a reply with it. if only bad keys are available, edward complains. however the complaint edward makes is that he does not have the user's public key. this is because there is currently no translation set for a more accurate message. some other refactoring has been performed as well. --- edward | 222 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 149 insertions(+), 73 deletions(-) diff --git a/edward b/edward index 3dd4d5b..25bce8c 100755 --- a/edward +++ b/edward @@ -149,6 +149,10 @@ class GPGData (object): 'sigkey_missing' is set to True if edward doesn't have the key it needs to verify the signature on a block of text. + 'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload + piece are not capable of encryption. This could happen if a key is revoked + or expired, for instance. + 'keys' is a list of fingerprints of keys obtained in public key blocks. """ @@ -157,6 +161,7 @@ class GPGData (object): plainobj = None sigs = [] sigkey_missing = False + key_cannot_encrypt = False keys = [] @@ -192,6 +197,9 @@ class ReplyInfo (object): 'sig_success' is set to True if edward could to some extent verify the signature of a signed part of the message to edward. + 'key_can_encrypt' is set to True if a key which can be encrypted to has + been found. + 'sig_failure' is set to True if edward could not verify a siganture. 'pubkey_success' is set to True if edward successfully imported a public @@ -200,6 +208,9 @@ class ReplyInfo (object): 'sigkey_missing' is set to True if edward doesn't have the public key needed for signature verification. + 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload + piece of the message are not capable of encryption. + 'have_repy_key' is set to True if edward has a public key to encrypt its reply to. """ @@ -214,10 +225,12 @@ class ReplyInfo (object): decrypt_success = False sig_success = False pubkey_success = False + key_can_encrypt = False decrypt_failure = False sig_failure = False sigkey_missing = False + key_cannot_encrypt = False have_reply_key = False @@ -573,8 +586,9 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): eddymsg_obj should have its payloads split into gpg and non-gpg pieces. Post: - Decryption, verification and key imports occur. the gpg_data member of - PayloadPiece objects get filled in with GPGData objects. + Decryption, verification and key imports occur. the gpg_data members of + PayloadPiece objects get filled in with GPGData objects with some of + their attributes set. """ if eddymsg_obj.multipart == True: @@ -594,9 +608,10 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): elif piece.piece_type == TxtType.message: piece.gpg_data = GPGData() - (plaintext_b, sigs, sigkey_missing) = decrypt_block(piece.string, gpgme_ctx) + (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if plaintext_b: piece.gpg_data.decrypted = True @@ -606,9 +621,10 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): continue # if not encrypted, check to see if this is an armored signature. - (plaintext_b, sigs, sigkey_missing) = verify_sig_message(piece.string, gpgme_ctx) + (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if plaintext_b: piece.piece_type = TxtType.signature @@ -617,19 +633,23 @@ def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx) elif piece.piece_type == TxtType.pubkey: - key_fps = add_gpg_key(piece.string, gpgme_ctx) + piece.gpg_data = GPGData() + + (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx) + + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if key_fps != []: - piece.gpg_data = GPGData() piece.gpg_data.keys = key_fps elif piece.piece_type == TxtType.detachedsig: piece.gpg_data = GPGData() for prev in prev_parts: - (sig_fps, sigkey_missing) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) + (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) piece.gpg_data.sigkey_missing = sigkey_missing + piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt if sig_fps != []: piece.gpg_data.sigs = sig_fps @@ -724,11 +744,11 @@ def prepare_for_reply_message (piece, replyinfo_obj): Post: replyinfo_obj gets updated with decryption status, signing status, a - potential signing key, and posession status of the public key for the - signature. + potential signing key, posession status of the public key for the + signature and encryption capability status if that key is missing. """ - if piece.gpg_data == None or piece.gpg_data.plainobj == None: + if piece.gpg_data.plainobj == None: replyinfo_obj.decrypt_failure = True return @@ -738,18 +758,21 @@ def prepare_for_reply_message (piece, replyinfo_obj): if replyinfo_obj.target_key != None: return - if piece.gpg_data.sigs != []: - replyinfo_obj.target_key = piece.gpg_data.sigs[0] - replyinfo_obj.sig_success = True - get_signed_part = False - - else: + if piece.gpg_data.sigs == []: if piece.gpg_data.sigkey_missing == True: replyinfo_obj.sigkey_missing = True + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True + # only include a signed message in the reply. get_signed_part = True + else: + replyinfo_obj.target_key = piece.gpg_data.sigs[0] + replyinfo_obj.sig_success = True + get_signed_part = False + flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part) # to catch public keys in encrypted blocks @@ -772,8 +795,9 @@ def prepare_for_reply_pubkey (piece, replyinfo_obj): replyinfo_obj has its fields updated. """ - if piece.gpg_data == None or piece.gpg_data.keys == []: - pass + if piece.gpg_data.keys == []: + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True else: replyinfo_obj.pubkey_success = True @@ -798,12 +822,15 @@ def prepare_for_reply_sig (piece, replyinfo_obj): replyinfo_obj has its fields updated. """ - if piece.gpg_data == None or piece.gpg_data.sigs == []: + if piece.gpg_data.sigs == []: replyinfo_obj.sig_failure = True if piece.gpg_data.sigkey_missing == True: replyinfo_obj.sigkey_missing = True + if piece.gpg_data.key_cannot_encrypt == True: + replyinfo_obj.key_cannot_encrypt = True + else: replyinfo_obj.sig_success = True @@ -879,9 +906,7 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx): gpgme_ctx: the gpgme context Return: - The key object of the key of either the target_key or the fallback one - if .target_key is not set. If the key cannot be loaded, then return - None. + Nothing Pre: Loading a key requires that we have the public key imported. This @@ -891,19 +916,28 @@ def get_key_from_fp (replyinfo_obj, gpgme_ctx): Post: If the key can be loaded, then replyinfo_obj.reply_to_key points to the public key object. If the key cannot be loaded, then the replyinfo_obj - is marked as having no public key available. + is marked as having no public key available. If the key is not capable + of encryption, it will not be used, and replyinfo_obj will be marked + accordingly. """ for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key): if key != None: try: encrypt_to_key = gpgme_ctx.get_key(key) + + except gpgme.GpgmeError: + continue + + if encrypt_to_key.can_encrypt == True: replyinfo_obj.encrypt_to_key = encrypt_to_key replyinfo_obj.have_reply_key = True + replyinfo_obj.key_can_encrypt = True return - except gpgme.GpgmeError: - pass + else: + replyinfo_obj.key_cannot_encrypt = True + def write_reply (replyinfo_obj): @@ -972,6 +1006,12 @@ def write_reply (replyinfo_obj): reply_plain += replyinfo_obj.replies['no_public_key'] reply_plain += "\n\n" + elif (replyinfo_obj.key_can_encrypt == False) \ + and (replyinfo_obj.key_cannot_encrypt == True): + debug('bad public key') + reply_plain += replyinfo_obj.replies['no_public_key'] + reply_plain += "\n\n" + if (reply_plain == ""): debug('plaintext message') @@ -996,7 +1036,9 @@ def add_gpg_key (key_block, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - the fingerprint(s) of the imported key(s) + the fingerprint(s) of the imported key(s) which can be used for + encryption, and a boolean marking whether none of the keys are capable + of encryption. """ fp = io.BytesIO(key_block.encode('ascii')) @@ -1008,15 +1050,26 @@ def add_gpg_key (key_block, gpgme_ctx): imports = [] key_fingerprints = [] + key_cannot_encrypt = False + + for import_res in imports: + fingerprint = import_res[0] + + try: + key_obj = gpgme_ctx.get_key(fingerprint) + except: + pass - if imports != []: - for import_ in imports: - fingerprint = import_[0] + if key_obj.can_encrypt == True: key_fingerprints += [fingerprint] + key_cannot_encrypt = False debug("added gpg key: " + fingerprint) - return key_fingerprints + elif key_fingerprints == []: + key_cannot_encrypt = True + + return (key_fingerprints, key_cannot_encrypt) def verify_sig_message (msg_block, gpgme_ctx): @@ -1033,10 +1086,11 @@ def verify_sig_message (msg_block, gpgme_ctx): Returns: A tuple containing the plaintext bytes of the signed part, the list of - fingerprints of keys signing the data, and a boolean marking whether - edward is missing all public keys for validating any of the signatures. - If verification failed, perhaps because the message was also encrypted, - then empty results are returned. + fingerprints of encryption-capable keys signing the data, a boolean + marking whether edward is missing all public keys for validating any of + the signatures, and a boolean marking whether all sigs' keys are + incapable of encryption. If verification failed, perhaps because the + message was also encrypted, sensible default values are returned. """ block_b = io.BytesIO(msg_block.encode('ascii')) @@ -1045,22 +1099,13 @@ def verify_sig_message (msg_block, gpgme_ctx): try: sigs = gpgme_ctx.verify(block_b, None, plain_b) except gpgme.GpgmeError: - return ("",[],False) + return ("",[],False,False) plaintext_b = plain_b.getvalue() - sigkey_missing = False - fingerprints = [] - for sig in sigs: - if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0): - fingerprints += [sig.fpr] - sigkey_missing = False - break - else: - if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0): - sigkey_missing = True + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) - return (plaintext_b, fingerprints, sigkey_missing) + return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): @@ -1074,32 +1119,25 @@ def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - A tuple containging a list of signing fingerprints if the signature - verification was sucessful, and a boolean marking whether edward is - missing all public keys for validating any of the signatures. - Otherwise, a tuple containing an empty list and True are returned. + A tuple containging a list of encryption capable signing fingerprints + if the signature verification was sucessful, a boolean marking whether + edward is missing all public keys for validating any of the signatures, + and a boolean marking whether all signing keys are incapable of + encryption. Otherwise, a tuple containing an empty list and True are + returned. """ detached_sig_fp = io.BytesIO(detached_sig.encode('ascii')) plaintext_fp = io.BytesIO(plaintext_bytes) try: - result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) + sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) except gpgme.GpgmeError: - return ([],False) + return ([],False,False) - sigkey_missing = False - sig_fingerprints = [] - for res_ in result: - if (res_.summary == 0) or (res_.summary & gpgme.SIGSUM_VALID != 0) or (res_.summary & gpgme.SIGSUM_GREEN != 0): - sig_fingerprints += [res_.fpr] - sigkey_missing = False - break - else: - if (res_.summary & gpgme.SIGSUM_KEY_MISSING != 0): - sigkey_missing = True + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) - return (sig_fingerprints, sigkey_missing) + return (fingerprints, sigkey_missing, key_cannot_encrypt) def decrypt_block (msg_block, gpgme_ctx): @@ -1112,10 +1150,11 @@ def decrypt_block (msg_block, gpgme_ctx): gpgme_ctx: the gpgme context Returns: - A tuple containing plaintext bytes, signatures (if the decryption and - signature verification were successful, respectively), and a boolean - marking whether edward is missing all public keys for validating any of - the signatures. + A tuple containing plaintext bytes, encryption-capable signatures (if + decryption and signature verification were successful, respectively), a + boolean marking whether edward is missing all public keys for + validating any of the signatures, and a boolean marking whether all + signature keys are incapable of encryption. """ block_b = io.BytesIO(msg_block.encode('ascii')) @@ -1124,22 +1163,59 @@ def decrypt_block (msg_block, gpgme_ctx): try: sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) except gpgme.GpgmeError: - return ("",[],False) + return ("",[],False,False) plaintext_b = plain_b.getvalue() + (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) + + return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) + + +def get_signature_fp (sigs, gpgme_ctx): + """Selects valid signatures from output of gpgme signature verifying functions + + get_signature_fp returns a list of valid signature fingerprints if those + fingerprints are associated with available keys capable of encryption. + + Args: + sigs: a signature verification result object list + gpgme_ctx: a gpgme context + + Returns: + fingerprints: a list of fingerprints + sigkey_missing: a boolean marking whether public keys are missing for + all available signatures. + key_cannot_encrypt: a boolearn marking whether available public keys are + incapable of encryption. + """ + sigkey_missing = False + key_cannot_encrypt = False fingerprints = [] + for sig in sigs: if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0): - fingerprints += [sig.fpr] - sigkey_missing = False - break - else: + try: + key_obj = gpgme_ctx.get_key(sig.fpr) + except: + if fingerprints == []: + sigkey_missing = True + continue + + if key_obj.can_encrypt == True: + fingerprints += [sig.fpr] + key_cannot_encrypt = False + sigkey_missing = False + + elif fingerprints == []: + key_cannot_encrypt = True + + elif fingerprints == []: if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0): sigkey_missing = True - return (plaintext_b, fingerprints, sigkey_missing) + return (fingerprints, sigkey_missing, key_cannot_encrypt) def email_to_from_subject (email_bytes): -- 2.25.1