def configure_gpg(self, key: str, password: Optional[str]) -> None: """Configure the repo to sign tags with GPG.""" home = os.environ["GNUPGHOME"] = mkdtemp(prefix="tagbot_gpg_") os.chmod(home, S_IREAD | S_IWRITE | S_IEXEC) logger.debug(f"Set GNUPGHOME to {home}") gpg = GPG(gnupghome=home, use_agent=True) import_result = gpg.import_keys(self._maybe_decode_private_key(key), passphrase=password) if import_result.sec_imported != 1: logger.warning(import_result.stderr) raise Abort("Importing key failed") key_id = import_result.fingerprints[0] logger.debug(f"GPG key ID: {key_id}") if password: # Sign some dummy data to put our password into the GPG agent, # so that we don't need to supply the password when we create a tag. sign_result = gpg.sign("test", passphrase=password) if sign_result.status != "signature created": logger.warning(sign_result.stderr) raise Abort("Testing GPG key failed") # On Debian, the Git version is too old to recognize tag.gpgSign, # so the tag command will need to use --sign. self._git._gpgsign = True self._git.config("tag.gpgSign", "true") self._git.config("user.signingKey", key_id)
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 _sign(self, data, digest_algo): gpg = GPG(homedir=settings.GPG_SIGN_DIR) if not gpg.list_keys(): # Import key if no private key key in keyring with open(settings.GPG_SIGN_KEY, 'r') as f: key = f.read() gpg.import_keys(key) signature = gpg.sign(data, passphrase=settings.GPG_SIGN_KEY_PASSPHRASE, clearsign=False, detach=True, digest_algo=digest_algo) return str(signature)
def configure_gpg(self, key: str, password: Optional[str]) -> None: """Configure the repo to sign tags with GPG.""" home = os.environ["GNUPGHOME"] = mkdtemp(prefix="tagbot_gpg_") os.chmod(home, S_IREAD | S_IWRITE | S_IEXEC) logger.debug(f"Set GNUPGHOME to {home}") gpg = GPG(gnupghome=home, use_agent=True) # For some reason, this doesn't require the password even though the CLI does. import_result = gpg.import_keys(self._maybe_b64(key)) if import_result.sec_imported != 1: logger.warning(import_result.stderr) raise Abort("Importing key failed") key_id = import_result.fingerprints[0] logger.debug(f"GPG key ID: {key_id}") if password: # Sign some dummy data to put our password into the GPG agent, # so that we don't need to supply the password when we create a tag. sign_result = gpg.sign("test", passphrase=password) if sign_result.status != "signature created": logger.warning(sign_result.stderr) raise Abort("Testing GPG key failed") self._git.config("user.signingKey", key_id) self._git.config("tag.gpgSign", "true")
def _main(): descr = 'Generate Gerrit release announcement email text' parser = argparse.ArgumentParser( description=descr, formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-v', '--version', dest='version', required=True, help='gerrit version to release') parser.add_argument('-p', '--previous', dest='previous', help='previous gerrit version (optional)') parser.add_argument('-s', '--summary', dest='summary', help='summary of the release content (optional)') options = parser.parse_args() summary = options.summary if summary and not summary.endswith("."): summary = summary + "." data = { "version": Version(options.version), "previous": options.previous, "summary": summary } war = os.path.join( os.path.expanduser("~/.m2/repository/com/google/gerrit/gerrit-war/"), "%(version)s/gerrit-war-%(version)s.war" % data) if not os.path.isfile(war): print("Could not find war file for Gerrit %s in local Maven repository" % data["version"], file=sys.stderr) sys.exit(1) md5 = hashlib.md5() sha1 = hashlib.sha1() sha256 = hashlib.sha256() BUF_SIZE = 65536 # Read data in 64kb chunks with open(war, 'rb') as f: while True: d = f.read(BUF_SIZE) if not d: break md5.update(d) sha1.update(d) sha256.update(d) data["sha1"] = sha1.hexdigest() data["sha256"] = sha256.hexdigest() data["md5"] = md5.hexdigest() template_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "release-announcement-template.txt") template = Template(open(template_path).read()) output = template.render(data=data) filename = "release-announcement-gerrit-%s.txt" % data["version"] with open(filename, "w") as f: f.write(output) gpghome = os.path.abspath(os.path.expanduser("~/.gnupg")) if not os.path.isdir(gpghome): print("Skipping signing due to missing gnupg home folder") else: try: gpg = GPG(homedir=gpghome) except TypeError: gpg = GPG(gnupghome=gpghome) signed = gpg.sign(output) filename = filename + ".asc" with open(filename, "w") as f: f.write(str(signed))
cnx = sqlite3.connect(db_file) cursor = cnx.cursor() p,e,c,ce,ec,s,sc,es,esc,cnt = (0,)*10 cursor.execute("select mtype || \" \" || message from statistics_messages where mtype='HELO_INFO' order by rtime desc limit %d"%tries) tries = 0 for (rec,) in cursor: plain_text = rec.encode("utf8") enc_data = str(gpg.encrypt(plain_text, public_key, always_trust=True)) enc_data_comp = zlib.compress(enc_data) comp_data = zlib.compress(plain_text) comp_data_enc = str(gpg.encrypt(comp_data, public_key, always_trust=True)) sign_data = str(gpg.sign(plain_text)) sign_data_comp = zlib.compress(sign_data) sign_enc_data = str(gpg.encrypt(plain_text, public_key, sign=public_key, always_trust=True)) sign_enc_data_comp = zlib.compress(sign_enc_data) p+=len(plain_text) e+=len(enc_data) c+=len(comp_data) ce+=len(comp_data_enc) ec+=len(enc_data_comp) s+=len(sign_data) sc+=len(sign_data_comp) es+=len(sign_enc_data) esc+=len(sign_enc_data_comp) if len(enc_data_comp)<len(enc_data):
class CryptoTxt: """Crypto operation provider for plaintext. We use GnuPG for now. Support for X.509 and other options might appear in the future. """ def __init__(self, gpg_binary, gpg_home): """Initialize the GnuPG instance.""" self.gpg_binary = gpg_binary self.gpg_home = gpg_home if not GPG: raise TracError(_("Unable to load the python-gnupg module. " "Please check and correct your installation.")) try: self.gpg = GPG(gpgbinary=self.gpg_binary, gnupghome=self.gpg_home) except ValueError: raise TracError(_("Missing the crypto binary. Please check and " "set full path with option 'gpg_binary'.")) else: # get list of available public keys once for later use self.pub_keys = self.gpg.list_keys() def sign(self, content, private_key=None): private_key = self._get_private_key(private_key) cipher = self.gpg.sign(content, keyid=private_key, passphrase='') return str(cipher) def encrypt(self, content, pubkeys): # always_trust needed for making it work with just any pubkey cipher = self.gpg.encrypt(content, pubkeys, always_trust=True) return str(cipher) def sign_encrypt(self, content, pubkeys, private_key=None): private_key = self._get_private_key(private_key) # always_trust needed for making it work with just any pubkey cipher = self.gpg.encrypt(content, pubkeys, always_trust=True, sign=private_key, passphrase='') return str(cipher) def get_pubkey_ids(self, addr): """Find public key with UID matching address to encrypt to.""" pubkey_ids = [] if self.pub_keys and 'uids' in self.pub_keys[-1] and \ 'fingerprint' in self.pub_keys[-1]: # compile pattern before use for better performance rcpt_re = re.compile(addr) for k in self.pub_keys: for uid in k['uids']: match = rcpt_re.search(uid) if match is not None: # check for key expiration if k['expires'] == '': pubkey_ids.append(k['fingerprint'][-16:]) elif (time.time() + 60) < float(k['expires']): pubkey_ids.append(k['fingerprint'][-16:]) break return pubkey_ids def _get_private_key(self, privkey=None): """Find private (secret) key to sign with.""" # read private keys from keyring privkeys = self.gpg.list_keys(True) # True => private keys if privkeys > 0 and 'fingerprint' in privkeys[-1]: fingerprints = [] for k in privkeys: fingerprints.append(k['fingerprint']) else: # no private key in keyring return None if privkey: # check for existence of private key received as argument # DEVEL: check for expiration as well if 7 < len(privkey) <= 40: for fp in fingerprints: if fp.endswith(privkey): # work with last 16 significant chars internally, # even if only 8 are required in trac.ini privkey = fp[-16:] break # no fingerprint matching key ID else: privkey = None else: # reset invalid key ID privkey = None else: # select (last) private key from keyring privkey = fingerprints[-1][-16:] return privkey
class CryptoTxt: """Crypto operation provider for plaintext. We use GnuPG for now. Support for X.509 and other options might appear in the future. """ def __init__(self, gpg_binary, gpg_home): """Initialize the GnuPG instance.""" self.gpg_binary = gpg_binary self.gpg_home = gpg_home if not GPG: raise TracError( _("Unable to load the python-gnupg module. " "Please check and correct your installation.")) try: self.gpg = GPG(gpgbinary=self.gpg_binary, gnupghome=self.gpg_home) except ValueError: raise TracError( _("Missing the crypto binary. Please check and " "set full path with option 'gpg_binary'.")) else: # get list of available public keys once for later use self.pub_keys = self.gpg.list_keys() def sign(self, content, private_key=None): private_key = self._get_private_key(private_key) cipher = self.gpg.sign(content, keyid=private_key, passphrase='') return str(cipher) def encrypt(self, content, pubkeys): # always_trust needed for making it work with just any pubkey cipher = self.gpg.encrypt(content, pubkeys, always_trust=True) return str(cipher) def sign_encrypt(self, content, pubkeys, private_key=None): private_key = self._get_private_key(private_key) # always_trust needed for making it work with just any pubkey cipher = self.gpg.encrypt(content, pubkeys, always_trust=True, sign=private_key, passphrase='') return str(cipher) def get_pubkey_ids(self, addr): """Find public key with UID matching address to encrypt to.""" pubkey_ids = [] if self.pub_keys and 'uids' in self.pub_keys[-1] and \ 'fingerprint' in self.pub_keys[-1]: # compile pattern before use for better performance rcpt_re = re.compile(addr) for k in self.pub_keys: for uid in k['uids']: match = rcpt_re.search(uid) if match is not None: # check for key expiration if k['expires'] == '': pubkey_ids.append(k['fingerprint'][-16:]) elif (time.time() + 60) < float(k['expires']): pubkey_ids.append(k['fingerprint'][-16:]) break return pubkey_ids def _get_private_key(self, privkey=None): """Find private (secret) key to sign with.""" # read private keys from keyring privkeys = self.gpg.list_keys(True) # True => private keys if privkeys > 0 and 'fingerprint' in privkeys[-1]: fingerprints = [] for k in privkeys: fingerprints.append(k['fingerprint']) else: # no private key in keyring return None if privkey: # check for existence of private key received as argument # DEVEL: check for expiration as well if 7 < len(privkey) <= 40: for fp in fingerprints: if fp.endswith(privkey): # work with last 16 significant chars internally, # even if only 8 are required in trac.ini privkey = fp[-16:] break # no fingerprint matching key ID else: privkey = None else: # reset invalid key ID privkey = None else: # select (last) private key from keyring privkey = fingerprints[-1][-16:] return privkey
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 gpg(object): """ module to wrap the gnupg library and gpg binary """ def __init__(self, folder_path=None, binary_path=None): # ugly but working if binary_path is None: binary_path = join(project_base, "gpg", "gpg.exe") if not isfile(binary_path): binary_path = join(project_base, 'gpg') if not isfile(binary_path): binary_path = find_executable('gpg') if not isfile(binary_path): raise RuntimeError('gpg not found') self.gpg = GPG( binary_path, # gpgbinary folder_path, # gnupghome verbose=False, options=["--allow-non-selfsigned-uid"]) def get_key(self, fingerprint, private, passphrase): """ returns the key belonging to the fingerprint given. if 'private' is True, the private key is returned. If 'private' is False, the public key will be returned. """ key = self.gpg.export_keys(fingerprint, private, armor=False, passphrase=passphrase) return b64encode(key) def add_keypair(self, public_key, private_key, site, user, passphrase): """ add a keypair into the gpg key database """ try: result1 = self.gpg.import_keys(b64decode(public_key)) result2 = self.gpg.import_keys(b64decode(private_key)) except TypeError as error: getLogger(__name__).critical("add_keypair TypeError " + str(error)) # make sure this is a key _pair_ try: assert result1.fingerprints[0] == result2.fingerprints[0] except (IndexError, AssertionError) as error: getLogger(__name__).exception( 'add_keypair IndexError/AssertionError: ' + str(error)) return None fingerprint = result1.fingerprints[0] if self.is_passphrase_valid(passphrase=passphrase, fingerprint=fingerprint): old_fingerprint = self.get_fingerprint(site, user) if not old_fingerprint: sql = "INSERT INTO keys (site, user, fingerprint) VALUES (?, ?, ?)" database_execute(sql, (site, user, fingerprint)) else: sql = "UPDATE keys SET SITE=? WHERE fingerprint=?" database_execute(sql, (site, old_fingerprint)) if fingerprint != old_fingerprint: getLogger(__name__).warn('updating %s fingerprint to %s' % (user, fingerprint)) return fingerprint else: return None def add_public_key(self, site, user, public_key): """ add a public key into the gpg key database """ try: result1 = self.gpg.import_keys(b64decode(public_key)) fingerprint = result1.fingerprints[0] sql = "INSERT INTO keys (site, user, fingerprint) VALUES (?, ?, ?)" database_execute(sql, (site, user, fingerprint)) return fingerprint except TypeError as error: getLogger(__name__).critical("add_public_key TypeError " + str(error)) except DatabaseError as error: getLogger(__name__).critical("add_public_key DatabaseError " + str(error)) def is_passphrase_valid(self, passphrase, label=None, user=None, fingerprint=None): if not fingerprint: fingerprint = self.get_fingerprint(label, user) sign_result = self.gpg.sign("test", keyid=fingerprint, passphrase=passphrase) return sign_result.data != '' def generate(self, passphrase, site, user): """ Generate a new 2048 bit GPG key and add it to the gpg manager. """ data = self.gpg.gen_key_input(key_length=2048, passphrase=passphrase) dat = self.gpg.gen_key(data) fingerprint = dat.fingerprint sql = "INSERT INTO keys (site, user, fingerprint) VALUES (?, ?, ?);" database_execute(sql, (site, user, fingerprint)) return fingerprint def get_fingerprint(self, site, user): sql = "select fingerprint from keys where site = ? and user = ?" result = database_execute(sql, (site, user)) if not len(result): return None return result[0][0] def has_key(self, site, user): """ Check whether a key is present for a certain site and user """ return self.get_fingerprint(site, user) is None def encrypt(self, data, site, user, armor=False): """ encrypt data for user at site. """ fingerprint = self.get_fingerprint(site, user) cryptdata = self.gpg.encrypt_file(StringIO(data), fingerprint, always_trust=True, armor=armor) return str(cryptdata) def decrypt(self, data, passphrase): """ decrypt data received from site. """ datafile = StringIO(data) result = self.gpg.decrypt_file(datafile, passphrase=passphrase, always_trust=True) return str(result) @staticmethod def add_pkcs7_padding(contents): # Input strings must be a multiple of the segment size 16 bytes in length segment_size = 16 # calculate how much padding is needed old_contents_length = len(contents) next_mult = old_contents_length + (segment_size - old_contents_length % segment_size) getLogger(__name__).debug( 'old contents length %s || new contents length %s' % (old_contents_length, next_mult)) # do the padding padding_byte = chr(next_mult - old_contents_length) contents = contents.ljust(next_mult, padding_byte) return contents @staticmethod def remove_pkcs7_padding(contents): """ Remove PKCS#7 padding bytes >>> gpg.remove_pkcs7_padding('some_content'.ljust(16, chr(4))) 'some_content' >>> gpg.remove_pkcs7_padding('some_content') 'some_content' >>> gpg.remove_pkcs7_padding('') '' :param contents: :return: contents without padding """ if len(contents) < 1: getLogger(__name__).debug( 'contents is empty. No PKCS#7 padding removed') return contents bytes_to_remove = ord(contents[-1]) # will work up to 255 bytes # check if contents have valid PKCS#7 padding if bytes_to_remove > 1 and contents[-2] != contents[-1]: getLogger(__name__).debug('no PKCS#7 padding detected') return contents getLogger(__name__).debug('removing %s bytes of PKCS#7 padding' % bytes_to_remove) return contents[0:(len(contents) - bytes_to_remove)]
class GnuPGMessage(EmailMessage): def __init__(self, *args, **kwargs): super(GnuPGMessage, self).__init__(*args, **kwargs) self.gpg = GPG(gnupghome=settings.GNUPG_HOMEDIR) def _normalize(self, original): return "\r\n".join(str(original).splitlines()[1:]) + "\r\n" def _sign(self, original): sig = self.gpg.sign( self._normalize(original), detach=True, clearsign=False) signature = MIMEApplication( str(sig), 'pgp-signature', encode_noop, name='signature.asc') signature.add_header('Content-Description', 'Digital signature') del signature['MIME-Version'] return signature def message(self): encoding = self.encoding or settings.DEFAULT_CHARSET msg = MIMEUTF8QPText(self.body, encoding) msg = self._create_message(msg) msg['Subject'] = self.subject msg['From'] = self.extra_headers.get('From', self.from_email) msg['To'] = self.extra_headers.get('To', ', '.join(self.to)) if self.cc: msg['Cc'] = ', '.join(self.cc) header_names = [key.lower() for key in self.extra_headers] if 'date' not in header_names: msg['Date'] = formatdate() if 'message-id' not in header_names: msg['Message-ID'] = make_msgid() for name, value in self.extra_headers.items(): if name.lower() in ('from', 'to'): # From and To are already handled continue msg[name] = value del msg['MIME-Version'] wrapper = SafeMIMEMultipart( 'signed', protocol='application/pgp-signature', micalg='pgp-sha512') wrapper.preamble = ( "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)" ) # copy headers from original message to PGP/MIME envelope for header in msg.keys(): if header.lower() not in ( 'content-disposition', 'content-type', 'mime-version' ): for value in msg.get_all(header): wrapper.add_header(header, value) del msg[header] for part in msg.walk(): del part['MIME-Version'] signature = self._sign(msg) wrapper['Content-Disposition'] = 'inline' wrapper.attach(msg) wrapper.attach(signature) return wrapper
def gpg_sign_message(message): gpg = GPG(gnupghome = KEYS_DIR) return gpg.sign(message, detach=True)
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)