#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2020 by the Linux Foundation # import sys import os import re import email import email.utils import email.message import smtplib import mailbox import b4 logger = b4.logger def create_attestation(cmdargs): attlines = list() subject = 'Patch attestation' for patchfile in cmdargs.patchfile: with open(patchfile, 'r', encoding='utf-8') as fh: content = fh.read() if content.find('From') != 0: logger.info('SKIP | %s', os.path.basename(patchfile)) continue msg = email.message_from_string(content) lmsg = b4.LoreMessage(msg) lmsg.load_hashes() att = lmsg.attestation if att is None: logger.info('SKIP | %s', os.path.basename(patchfile)) # See if it's a cover letter if lmsg.counters_inferred or lmsg.counter > 0: # No continue newprefs = list() for prefix in lmsg.lsubject.prefixes: if prefix.lower() == 'patch': newprefs.append('PSIGN') elif prefix == '%s/%s' % (lmsg.counter, lmsg.expected): newprefs.append('X/%s' % lmsg.expected) else: newprefs.append(prefix) subject = '[%s] %s' % (' '.join(newprefs), lmsg.subject) continue logger.info('HASH | %s', os.path.basename(patchfile)) attlines.append('%s:' % att.attid) attlines.append(' i: %s' % att.i) attlines.append(' m: %s' % att.m) attlines.append(' p: %s' % att.p) payload = '\n'.join(attlines) usercfg = b4.get_user_config() gpgargs = list() if 'signingkey' in usercfg: gpgargs += ['-u', usercfg['signingkey']] gpgargs += ['--clearsign', '--comment', 'att-fmt-ver: %s' % b4.ATTESTATION_FORMAT_VER, '--comment', 'att-hash: sha256', ] ecode, signed = b4.gpg_run_command(gpgargs, stdin=payload.encode('utf-8')) if ecode > 0: config = b4.get_main_config() logger.critical('ERROR: Unable to sign using %s', config['gpgbin']) sys.exit(1) att_msg = email.message.EmailMessage() att_msg.set_payload(signed.encode('utf-8')) sender = cmdargs.sender if '>' not in sender: sender = '<%s>' % sender att_msg['From'] = sender att_msg['To'] = '' att_msg['Message-Id'] = email.utils.make_msgid(domain='kernel.org') att_msg['Subject'] = subject logger.info('---') if not cmdargs.nosubmit: # Try to deliver it via mail.kernel.org try: mailserver = smtplib.SMTP('mail.kernel.org', 587) # identify ourselves to smtp gmail client mailserver.ehlo() # secure our email with tls encryption mailserver.starttls() # re-identify ourselves as an encrypted connection mailserver.ehlo() logger.info('Delivering via mail.kernel.org') mailserver.sendmail('devnull@kernel.org', 'signatures@kernel.org', att_msg.as_string()) mailserver.quit() sys.exit(0) except Exception as ex: logger.info('Could not deliver: %s', ex) # Future iterations will also be able to submit this to a RESTful URL # at git.kernel.org, in order not to depend on avaialbility of SMTP gateways with open(cmdargs.output, 'wb') as fh: fh.write(att_msg.as_bytes()) logger.info('Wrote %s', cmdargs.output) logger.info('You can send it using:') logger.info(' sendmail -oi signatures@kernel.org < %s', cmdargs.output) logger.info(' mutt -H %s', cmdargs.output) def verify_attestation(cmdargs): config = b4.get_main_config() if cmdargs.tofu: config['attestation-trust-model'] = 'tofu' exact_from_match = True if cmdargs.ignorefrom: exact_from_match = False mbx = mailbox.mbox(cmdargs.mbox[0]) if cmdargs.attfile: b4.LoreAttestationDocument.load_from_file(cmdargs.attfile) eligible = list() for msg in mbx: lmsg = b4.LoreMessage(msg) if lmsg.has_diff: eligible.append(lmsg) continue # See if body has "att-fmt-ver if re.search(r'^Comment: att-fmt-ver:', lmsg.body, re.I | re.M): logger.debug('Found attestation message') b4.LoreAttestationDocument.load_from_string(lmsg.msgid, lmsg.body) logger.debug('SKIP | %s', msg['Subject']) if not len(eligible): logger.error('No patches found in %s', cmdargs.mbox[0]) sys.exit(1) logger.info('---') attrailers = set() ecode = 1 if config['attestation-checkmarks'] == 'fancy': attpass = b4.PASS_FANCY attfail = b4.FAIL_FANCY else: attpass = b4.PASS_SIMPLE attfail = b4.FAIL_SIMPLE for lmsg in eligible: attdoc = lmsg.get_attestation(lore_lookup=True, exact_from_match=exact_from_match) if not attdoc: logger.critical('%s %s', attfail, lmsg.full_subject) if not cmdargs.nofast: logger.critical('Aborting due to failure.') ecode = 1 break else: ecode = 128 continue if ecode != 128: ecode = 0 logger.critical('%s %s', attpass, lmsg.full_subject) attrailers.add(attdoc.lsig.attestor.get_trailer(lmsg.fromemail)) logger.critical('---') if ecode > 0: logger.critical('Attestation verification failed.') errors = set() for attdoc in b4.ATTESTATIONS: errors.update(attdoc.errors) if len(errors): logger.critical('---') logger.critical('The validation process reported the following errors:') for error in errors: logger.critical(' %s %s', attfail, error) else: logger.critical('All patches passed attestation:') for attrailer in attrailers: logger.critical(' %s %s', attpass, attrailer) sys.exit(ecode)