def verify_key(email): ''' fetch user's GPG key and make sure it matches given email address ''' gpgkey = None gpg = GPG() # pylint: disable=no-member verified = gpg.verify(gpg.sign('', keyid=email).data) logging.debug('verified: %s', verified) if not verified.username.endswith('<' + email + '>'): raise ValueError('%s no match for GPG certificate %s' % (email, verified.username)) gpgkey = verified.key_id return gpgkey
def verify_wordlist(wordlist): JOECHRISELLIS_KEY_ID = "02C6372546042BC2" try: from gnupg import GPG except ImportError: print "python-gnupg is not installed." print "Please verify the wordlist manually." print "$ gpg --verify", wordlist.name exit(1) gpg = GPG() verified = gpg.verify(wordlist.read()) if verified.key_id == JOECHRISELLIS_KEY_ID: print "Correct signature from", verified.username print "Signature OK" elif verified.key_id: print "Signature was verified, however, the signing key used " \ "is not owned by the author of this program." print "The wordlist was signed by '%s'" % (verified.key_id) print "Please continue with caution!" else: print "Invalid/no signature!"
class GPGMail(object): def __init__(self, gpg=None): if gpg: self.gpg = gpg else: self.gpg = GPG(gpgbinary="gpg2", use_agent=True) GPGLogger.setLevel(logging.DEBUG) self.logger = logging.getLogger('GPGMail') def _armor(self, container, message, signature): """ Make the armor signed message """ if container.get_param('protocol') == 'application/pgp-signature': m = re.match(r'^pgp-(.*)$', container.get_param('micalg')) if m: TEMPLATE = '-----BEGIN PGP SIGNED MESSAGE-----\n' \ 'Hash: %s\n\n' \ '%s\n%s\n' s = StringIO() text = re.sub(r'(?m)^(-.*)$', r'- \1', self._flatten(message)) s.write(TEMPLATE % (m.group(1).upper(), text, signature.get_payload())) return s.getvalue() return None def _filter_parts(sefl, m, f): """Iterate over messages that satisfy predicate.""" for x in m.walk(): if f(x): yield x def _flatten(self, message): """Return raw string representation of message.""" try: s = StringIO() g = Generator(s, mangle_from_=False, maxheaderlen=0) g.flatten(message) return s.getvalue() finally: s.close() def _signed_parts(self, message): """Iterate over signed parts of message yielding GPG verification status and signed contents.""" f = lambda m: \ m.is_multipart() and m.get_content_type() == 'multipart/signed' \ or not m.is_multipart() and m.get_content_maintype() == 'text' for part in self._filter_parts(message, f): if part.is_multipart(): try: signed_part, signature = part.get_payload() s = None sign_type = signature.get_content_type() if sign_type == 'application/pgp-signature': s = self._armor(part, signed_part, signature) yield self.gpg.verify(s), True, signature.get_filename() except ValueError: pass else: payload = part.get_payload(decode=True) yield self.gpg.verify(payload), False, None def verify(self, message): """Verify signature of a email message and returns the GPG info""" result = {} for verified, sign_attached, filename in self._signed_parts(message): if verified is not None: result = verified.__dict__ break if 'status' in result and result['status'] is None: return None if 'gpg' in result: del(result['gpg']) if sign_attached: result['filename'] = filename return result def _get_digest_algo(self, signature): """ Returns a string representation of the digest algo used in signature. Raises a TypeError if signature.hash_algo does not exists. Acceptable values for signature.hash_algo are: MD5 1 SHA1 2 RMD160 3 SHA256 8 SHA384 9 SHA512 10 SHA224 11 See gnupg/include/cipher.h for more info """ values = { 1: "MD5", 2: "SHA1", 3: "RMD160", 8: "SHA256", 9: "SHA384", 10: "SHA512", 11: "SHA224" } hash_algo = signature.hash_algo try: if isinstance(hash_algo, (str, unicode)): hash_algo = int(hash_algo) return values[hash_algo] except: raise TypeError("Invalid signature hash_algo {}".format(hash_algo)) def sign(self, message): """Sign a email message and return the new message signed as string""" if isinstance(message, (str, unicode)): message = email.message_from_string(message) # Create the basetxt, which contains the original body message # If original message is multipart, take the # Content-Type and the Body - ignore other Headers.. if message.is_multipart(): content_type = 'Content-Type: {}'.format(message['Content-Type']) try: body = self._flatten(message).split('\n\n', 1)[1] except: raise ValueError("Message cannot be split in Headers and Body") basetxt = '{}\n\n{}'.format(content_type, body) # else get only the body message else: basetxt = MIMEUTF8QPText(message).as_string() # See RFC 3156 (Section 5.) to understand these transformations # 1. all the lines must end with <CR><LF> basetxt = basetxt.replace('\r\n', '\n')\ .replace('\n', '\r\n') # 2. remove trailing spaces basetxt = re.sub(r' +\r\n', '\r\n', basetxt, flags=re.M) # signing signature = self.gpg.sign(basetxt, detach=True) self.logger.error("signature %s %s" % (basetxt, signature)) # create the new message as multipart/signed (see RFC 3156) micalg = "pgp-{}".format(self._get_digest_algo(signature).lower()) new_message = MIMEMultipart(_subtype="signed", micalg=micalg, protocol="application/pgp-signature") # copy the headers header_to_copy = [ "Date", "Subject", "From", "To", "Bcc", "Cc", "Reply-To", "References", "In-Reply-To" ] for h in header_to_copy: if h in message: new_message[h] = message[h] # attach signed_part and signature and return message as string return self._attach_signed_parts(new_message, basetxt, signature) def _attach_signed_parts(self, message, signed_part, signature): """ Attach the signed_part and signature to the message (MIMEMultipart) and returns the new message as str """ # According with RFC 3156 the signed_part in the message # must be equal to the signed one. # The best way to do this is "hard" attach the parts # using strings. if not isinstance(signature, (str, unicode)): signature = str(signature) # get the body of the MIMEMultipart message, # remove last lines which close the boundary msg_lines = message.as_string().split('\n')[:-3] # get the last opening boundary boundary = msg_lines.pop() # create the signature as attachment sigmsg = Message() sigmsg['Content-Type'] = 'application/pgp-signature; ' + \ 'name="signature.asc"' sigmsg['Content-Description'] = 'GooPG digital signature' sigmsg.set_payload(signature) # attach the signed_part msg_lines += [boundary, signed_part] # attach the signature msg_lines += [boundary, sigmsg.as_string(), '{}--'.format(boundary)] # return message a string return '\n'.join(msg_lines)
class GPGMail(object): def __init__(self, gpg=None): if gpg: self.gpg = gpg else: self.gpg = GPG(gpgbinary="gpg2", use_agent=True) GPGLogger.setLevel(logging.DEBUG) self.logger = logging.getLogger('GPGMail') def _armor(self, container, message, signature): """ Make the armor signed message """ if container.get_param('protocol') == 'application/pgp-signature': m = re.match(r'^pgp-(.*)$', container.get_param('micalg')) if m: TEMPLATE = '-----BEGIN PGP SIGNED MESSAGE-----\n' \ 'Hash: %s\n\n' \ '%s\n%s\n' s = StringIO() text = re.sub(r'(?m)^(-.*)$', r'- \1', self._flatten(message)) s.write(TEMPLATE % (m.group(1).upper(), text, signature.get_payload())) return s.getvalue() return None def _filter_parts(sefl, m, f): """Iterate over messages that satisfy predicate.""" for x in m.walk(): if f(x): yield x def _flatten(self, message): """Return raw string representation of message.""" try: s = StringIO() g = Generator(s, mangle_from_=False, maxheaderlen=0) g.flatten(message) return s.getvalue() finally: s.close() def _signed_parts(self, message): """Iterate over signed parts of message yielding GPG verification status and signed contents.""" f = lambda m: \ m.is_multipart() and m.get_content_type() == 'multipart/signed' \ or not m.is_multipart() and m.get_content_maintype() == 'text' for part in self._filter_parts(message, f): if part.is_multipart(): try: signed_part, signature = part.get_payload() s = None sign_type = signature.get_content_type() if sign_type == 'application/pgp-signature': s = self._armor(part, signed_part, signature) yield self.gpg.verify( s), True, signature.get_filename() except ValueError: pass else: payload = part.get_payload(decode=True) yield self.gpg.verify(payload), False, None def verify(self, message): """Verify signature of a email message and returns the GPG info""" result = {} for verified, sign_attached, filename in self._signed_parts(message): if verified is not None: result = verified.__dict__ break if 'status' in result and result['status'] is None: return None if 'gpg' in result: del (result['gpg']) if sign_attached: result['filename'] = filename return result def _get_digest_algo(self, signature): """ Returns a string representation of the digest algo used in signature. Raises a TypeError if signature.hash_algo does not exists. Acceptable values for signature.hash_algo are: MD5 1 SHA1 2 RMD160 3 SHA256 8 SHA384 9 SHA512 10 SHA224 11 See gnupg/include/cipher.h for more info """ values = { 1: "MD5", 2: "SHA1", 3: "RMD160", 8: "SHA256", 9: "SHA384", 10: "SHA512", 11: "SHA224" } hash_algo = signature.hash_algo try: if isinstance(hash_algo, (str, unicode)): hash_algo = int(hash_algo) return values[hash_algo] except: raise TypeError("Invalid signature hash_algo {}".format(hash_algo)) def sign(self, message): """Sign a email message and return the new message signed as string""" if isinstance(message, (str, unicode)): message = email.message_from_string(message) # Create the basetxt, which contains the original body message # If original message is multipart, take the # Content-Type and the Body - ignore other Headers.. if message.is_multipart(): content_type = 'Content-Type: {}'.format(message['Content-Type']) try: body = self._flatten(message).split('\n\n', 1)[1] except: raise ValueError("Message cannot be split in Headers and Body") basetxt = '{}\n\n{}'.format(content_type, body) # else get only the body message else: basetxt = MIMEUTF8QPText(message).as_string() # See RFC 3156 (Section 5.) to understand these transformations # 1. all the lines must end with <CR><LF> basetxt = basetxt.replace('\r\n', '\n')\ .replace('\n', '\r\n') # 2. remove trailing spaces basetxt = re.sub(r' +\r\n', '\r\n', basetxt, flags=re.M) # signing signature = self.gpg.sign(basetxt, detach=True) self.logger.error("signature %s %s" % (basetxt, signature)) # create the new message as multipart/signed (see RFC 3156) micalg = "pgp-{}".format(self._get_digest_algo(signature).lower()) new_message = MIMEMultipart(_subtype="signed", micalg=micalg, protocol="application/pgp-signature") # copy the headers header_to_copy = [ "Date", "Subject", "From", "To", "Bcc", "Cc", "Reply-To", "References", "In-Reply-To" ] for h in header_to_copy: if h in message: new_message[h] = message[h] # attach signed_part and signature and return message as string return self._attach_signed_parts(new_message, basetxt, signature) def _attach_signed_parts(self, message, signed_part, signature): """ Attach the signed_part and signature to the message (MIMEMultipart) and returns the new message as str """ # According with RFC 3156 the signed_part in the message # must be equal to the signed one. # The best way to do this is "hard" attach the parts # using strings. if not isinstance(signature, (str, unicode)): signature = str(signature) # get the body of the MIMEMultipart message, # remove last lines which close the boundary msg_lines = message.as_string().split('\n')[:-3] # get the last opening boundary boundary = msg_lines.pop() # create the signature as attachment sigmsg = Message() sigmsg['Content-Type'] = 'application/pgp-signature; ' + \ 'name="signature.asc"' sigmsg['Content-Description'] = 'GooPG digital signature' sigmsg.set_payload(signature) # attach the signed_part msg_lines += [boundary, signed_part] # attach the signature msg_lines += [boundary, sigmsg.as_string(), '{}--'.format(boundary)] # return message a string return '\n'.join(msg_lines)