Cleanups and extra checks Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
diff --git a/.gitignore b/.gitignore index 9769964..74574e2 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,3 +1,4 @@ *.pyc __pycache__ .idea +*~
diff --git a/.devkey/kernel.org/dev/patches.txt b/.keys/devkey/kernel.org/dev/patches.txt similarity index 100% rename from .devkey/kernel.org/dev/patches.txt rename to .keys/devkey/kernel.org/dev/patches.txt
diff --git a/.keys/openpgp/graphs/E63EDCA9329DD07E.svg b/.keys/openpgp/graphs/E63EDCA9329DD07E.svg new file mode 100644 index 0000000..1c50c80 --- /dev/null +++ b/.keys/openpgp/graphs/E63EDCA9329DD07E.svg
@@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (0) + --> +<!-- Title: G Pages: 1 --> +<svg width="216pt" height="166pt" + viewBox="0.00 0.00 216.00 166.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 162)"> +<title>G</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-162 212,-162 212,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_toplevel</title> +<polygon fill="none" stroke="#ffffff" points="8,-81 8,-150 200,-150 200,-81 8,-81"/> +</g> +<!-- a_387 --> +<g id="node1" class="node"> +<title>a_387</title> +<path fill="none" stroke="#c0c0c0" d="M25,-.5C25,-.5 183,-.5 183,-.5 189,-.5 195,-6.5 195,-12.5 195,-12.5 195,-40.5 195,-40.5 195,-46.5 189,-52.5 183,-52.5 183,-52.5 25,-52.5 25,-52.5 19,-52.5 13,-46.5 13,-40.5 13,-40.5 13,-12.5 13,-12.5 13,-6.5 19,-.5 25,-.5"/> +<text text-anchor="middle" x="104" y="-39.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">Konstantin Ryabitsev</text> +<text text-anchor="middle" x="104" y="-27.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">linuxfoundation.org</text> +<polyline fill="none" stroke="#c0c0c0" points="13,-20.5 195,-20.5 "/> +<text text-anchor="middle" x="44.5" y="-7.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">RSA 4096</text> +<polyline fill="none" stroke="#c0c0c0" points="76,-.5 76,-20.5 "/> +<text text-anchor="middle" x="135.5" y="-7.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">E63EDCA9329DD07E</text> +</g> +<!-- a_211 --> +<g id="node2" class="node"> +<title>a_211</title> +<path fill="none" stroke="#c0c0c0" d="M28,-89.5C28,-89.5 180,-89.5 180,-89.5 186,-89.5 192,-95.5 192,-101.5 192,-101.5 192,-129.5 192,-129.5 192,-135.5 186,-141.5 180,-141.5 180,-141.5 28,-141.5 28,-141.5 22,-141.5 16,-135.5 16,-129.5 16,-129.5 16,-101.5 16,-101.5 16,-95.5 22,-89.5 28,-89.5"/> +<text text-anchor="middle" x="104" y="-128.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">Linus Torvalds</text> +<text text-anchor="middle" x="104" y="-116.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">kernel.org</text> +<polyline fill="none" stroke="#c0c0c0" points="16,-109.5 192,-109.5 "/> +<text text-anchor="middle" x="47.5" y="-96.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">RSA 2048</text> +<polyline fill="none" stroke="#c0c0c0" points="79,-89.5 79,-109.5 "/> +<text text-anchor="middle" x="135.5" y="-96.7" font-family="droid sans,dejavu sans,helvetica" font-size="11.00" fill="#000000">79BE3E4300411886</text> +</g> +<!-- a_211->a_387 --> +<g id="edge1" class="edge"> +<title>a_211->a_387</title> +<path fill="none" stroke="#000000" d="M104,-89.2784C104,-80.9495 104,-71.5865 104,-62.6839"/> +<polygon fill="#000000" stroke="#000000" points="107.5001,-62.5341 104,-52.5341 100.5001,-62.5342 107.5001,-62.5341"/> +</g> +</g> +</svg>
diff --git a/mricon.asc b/.keys/openpgp/keys/E63EDCA9329DD07E.asc similarity index 100% rename from mricon.asc rename to .keys/openpgp/keys/E63EDCA9329DD07E.asc
diff --git a/.keys/openpgp/pubring.kbx b/.keys/openpgp/pubring.kbx new file mode 100644 index 0000000..2a7c5ef --- /dev/null +++ b/.keys/openpgp/pubring.kbx Binary files differ
diff --git a/emails/mricon-signed-pgp-commit-message-modified.eml b/emails/mricon-signed-pgp-commit-message-modified.eml new file mode 100644 index 0000000..7a5c824 --- /dev/null +++ b/emails/mricon-signed-pgp-commit-message-modified.eml
@@ -0,0 +1,55 @@ +Return-Path: <SRS0=87nF=CT=vger.kernel.org=linux-kernel-owner@kernel.org> +From: Konstantin Ryabitsev <mricon@kernel.org> +To: <linux-kernel@vger.kernel.org> +Subject: [PATCH] drm: xlnx: remove defined but not used 'scaling_factors_666' +Date: Thu, 10 Sep 2020 22:06:30 +0800 +Message-ID: <patch-attestation-examples@kernel.org> +X-Mailer: git-send-email 2.25.4 +X-Patch-Hashes: v=1; h=sha256; + i=pkD5Pg8+cndZAzQQzo3RBSOOUzZM3GYWxiFIKFGIKe0=; + m=yW4TvC/DGWCUJTa11Aw1b/2ZAXobsLD45aLA/440yQI=; + p=iJdYN6+isP/3HmQaf1IiG7OfA1vzRxXlPGZtvecS484= +X-Patch-Sig: m=pgp; i=mricon@kernel.org; s=0xE63EDCA9329DD07E; + b=iHUEABYIAB0WIQR2vl2yUnHhSB5njDW2xBzjVmSZbAUCX1+/nQAKCRC2xBzjVmSZbFiQAQD42c + l5It3AVJbtkwbY5XZxb9I9YuvvX3L3buU+EwjumwD9HBH8t6xcavIKQF6dwKjsmhwJnDj1tCfaxg + 3WRdUllgM= +MIME-Version: 1.0 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +From: Dev Eloper <dev@kernel.org> + +This addresses the following gcc warning with "make W=1": +xxx + +drivers/gpu/drm/xlnx/zynqmp_disp.c:245:18: warning: +‘scaling_factors_666’ defined but not used [-Wunused-const-variable=] + 245 | static const u32 scaling_factors_666[] = { + | ^~~~~~~~~~~~~~~~~~~ + +Signed-off-by: Dev Eloper <dev@kernel.org> +--- + drivers/gpu/drm/xlnx/zynqmp_disp.c | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/drivers/gpu/drm/xlnx/zynqmp_disp.c b/drivers/gpu/drm/xlnx/zynqmp_disp.c +index a455cfc1bee5..98bd48f13fd1 100644 +--- a/drivers/gpu/drm/xlnx/zynqmp_disp.c ++++ b/drivers/gpu/drm/xlnx/zynqmp_disp.c +@@ -242,12 +242,6 @@ static const u32 scaling_factors_565[] = { + ZYNQMP_DISP_AV_BUF_5BIT_SF, + }; + +-static const u32 scaling_factors_666[] = { +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +-}; +- + static const u32 scaling_factors_888[] = { + ZYNQMP_DISP_AV_BUF_8BIT_SF, + ZYNQMP_DISP_AV_BUF_8BIT_SF, +-- +2.25.4 + +
diff --git a/emails/mricon-signed-pgp-patch-modified.eml b/emails/mricon-signed-pgp-patch-modified.eml new file mode 100644 index 0000000..e3a4c19 --- /dev/null +++ b/emails/mricon-signed-pgp-patch-modified.eml
@@ -0,0 +1,54 @@ +Return-Path: <SRS0=87nF=CT=vger.kernel.org=linux-kernel-owner@kernel.org> +From: Konstantin Ryabitsev <mricon@kernel.org> +To: <linux-kernel@vger.kernel.org> +Subject: [PATCH] drm: xlnx: remove defined but not used 'scaling_factors_666' +Date: Thu, 10 Sep 2020 22:06:30 +0800 +Message-ID: <patch-attestation-examples@kernel.org> +X-Mailer: git-send-email 2.25.4 +X-Patch-Hashes: v=1; h=sha256; + i=pkD5Pg8+cndZAzQQzo3RBSOOUzZM3GYWxiFIKFGIKe0=; + m=yW4TvC/DGWCUJTa11Aw1b/2ZAXobsLD45aLA/440yQI=; + p=iJdYN6+isP/3HmQaf1IiG7OfA1vzRxXlPGZtvecS484= +X-Patch-Sig: m=pgp; i=mricon@kernel.org; s=0xE63EDCA9329DD07E; + b=iHUEABYIAB0WIQR2vl2yUnHhSB5njDW2xBzjVmSZbAUCX1+/nQAKCRC2xBzjVmSZbFiQAQD42c + l5It3AVJbtkwbY5XZxb9I9YuvvX3L3buU+EwjumwD9HBH8t6xcavIKQF6dwKjsmhwJnDj1tCfaxg + 3WRdUllgM= +MIME-Version: 1.0 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +From: Dev Eloper <dev@kernel.org> + +This addresses the following gcc warning with "make W=1": + +drivers/gpu/drm/xlnx/zynqmp_disp.c:245:18: warning: +‘scaling_factors_666’ defined but not used [-Wunused-const-variable=] + 245 | static const u32 scaling_factors_666[] = { + | ^~~~~~~~~~~~~~~~~~~ + +Signed-off-by: Dev Eloper <dev@kernel.org> +--- + drivers/gpu/drm/xlnx/zynqmp_disp.c | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/drivers/gpu/drm/xlnx/zynqmp_disp.c b/drivers/gpu/drm/xlnx/zynqmp_disp.c +index a455cfc1bee5..98bd48f13fd1 100644 +--- a/drivers/gpu/drm/xlnx/zynqmp_disp.c ++++ b/drivers/gpu/drm/xlnx/zynqmp_disp.c +@@ -242,12 +242,6 @@ static const u32 scaling_factors_565[] = { + ZYNQMP_DISP_AV_BUF_5BIT_SF, xxx + }; + +-static const u32 scaling_factors_666[] = { +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +- ZYNQMP_DISP_AV_BUF_6BIT_SF, +-}; +- + static const u32 scaling_factors_888[] = { + ZYNQMP_DISP_AV_BUF_8BIT_SF, + ZYNQMP_DISP_AV_BUF_8BIT_SF, +-- +2.25.4 + +
diff --git a/main.py b/main.py index 4f84adb..2807be0 100755 --- a/main.py +++ b/main.py
@@ -4,6 +4,7 @@ import base64 import email import email.header +import email.utils import b4 import time import dkim @@ -15,6 +16,7 @@ import requests import anybase32 import urllib.parse +import logging from nacl.signing import SigningKey, VerifyKey from nacl.encoding import Base64Encoder @@ -26,25 +28,68 @@ from Cryptodome.Hash import SHA256 from Cryptodome.PublicKey import RSA +from typing import Tuple + XPH_HDR = 'X-Patch-Hashes' XPS_HDR = 'X-Patch-Sig' +logger = logging.getLogger(__name__) -def _run_command(cmdargs, stdin=None): + +def _run_command(cmdargs: list, stdin: bytes = None) -> Tuple[int, bytes, bytes]: sp = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) (output, error) = sp.communicate(input=stdin) return sp.returncode, output, error -def gpg_run_command(cmdargs, stdin=None): +def gpg_run_command(cmdargs: list, stdin: bytes = None) -> Tuple[int, bytes, bytes]: gpgbin = 'gpg' cmdargs = [gpgbin, '--batch', '--no-auto-key-retrieve', '--no-auto-check-trustdb'] + cmdargs return _run_command(cmdargs, stdin) -def get_dkim_key(domain, selector, wantkey='rsa', timeout=5): +def load_message(msgfile: str): + with open(msgfile, 'rb') as fh: + logger.info('Using %s as message source', msgfile) + contents = fh.read() + return email.message_from_bytes(contents) + + +def splitter(longstr: str, limit: int = 77) -> str: + splitstr = list() + first = True + while len(longstr) > limit: + at = limit + if first: + first = False + at -= 2 + splitstr.append(longstr[:at]) + longstr = longstr[at:] + splitstr.append(longstr) + return ' '.join(splitstr) + + +def verify_identity_domain(msg, identity: str, domain: str) -> bool: + # Domain is supposed to be present in identity + if not identity.endswith(domain): + logger.critical('FAIL : domain (d=%s) is not in identity (i=%s)', domain, identity) + return False + fromeml = email.utils.getaddresses(msg.get_all('from', []))[0][1] + if identity.find('@') < 0: + logger.critical('FAIL : identity must contain @ (i=%s)', identity) + return False + ilocal, idomain = identity.split('@') + # identity is supposed to be present in from + if not fromeml.endswith(f'@{idomain}'): + logger.critical('FAIL : identity (i=%s) does not match from (from=%s)', identity, fromeml) + return False + logger.info('PASS : identity and domain match From header') + return True + + +def get_dkim_key(domain: str, selector: str, wantkey: str = 'rsa', timeout: int = 5) -> str: name = f'{selector}._domainkey.{domain}.' - print(f'DNS-lookup: {name}') + logger.info('DNS-lookup: %s', name) keydata = None try: a = dns.resolver.resolve(name, dns.rdatatype.TXT, raise_on_no_answer=False, lifetime=timeout) @@ -62,69 +107,81 @@ if keydata: break except dns.resolver.NXDOMAIN: - print(f'Domain {name} does not exist') + logger.critical('Domain %s does not exist', name) sys.exit(1) pass if not keydata: - print(f'Domain {name} does not contain a DKIM record') + logger.critical('Domain %s does not contain a DKIM record', name) sys.exit(1) parts = get_parts_from_header(keydata) if 'p' not in parts: - print(f'Domain {name} does not contain a DKIM key') + logger.critical('Domain %s does not contain a DKIM key', name) sys.exit(1) return parts['p'] -def get_wk_key(domain, selector, timeout=5): +def get_git_toplevel(gitdir: str = None) -> str: + cmdargs = ['git'] + if gitdir: + cmdargs += ['--git-dir', gitdir] + cmdargs += ['rev-parse', '--show-toplevel'] + ecode, out, err = _run_command(cmdargs) + if ecode == 0: + return out.decode().strip() + return '' + + +def get_wk_key(domain: str, selector: str, timeout: int = 5) -> str: wkurl = f'https://{domain}/.well-known/_domainkey/{selector}.txt' - print(f'Retrieving: {wkurl}') + logger.info('Retrieving: %s', wkurl) res = requests.get(wkurl, timeout=timeout) if res.status_code != 200: - print(f'Could not get {wkurl}') + logger.info('Could not retrieve %s: %s', wkurl, res.status_code) sys.exit(1) keydata = res.content.decode().strip() + logger.debug('keydata: %s', keydata) parts = get_parts_from_header(keydata) return parts['p'] -def get_wkd_key(domain, identity, selector, timeout=5): +def get_wkd_key(domain: str, identity: str, selector: str, timeout: int = 5) -> str: identity = identity.split('@')[0] # Attempt to load from local git dir if we are in a git dir - cmdargs = ['git', 'rev-parse', '--show-toplevel'] - ecode, out, err = _run_command(cmdargs) - if ecode == 0: + gittop = get_git_toplevel() + if gittop: # urlencode domain/identity/selector to make sure nobody tries path-based badness - subpath = os.path.join(out.decode().strip(), '.devkey', + subpath = os.path.join(gittop, '.keys', 'devkey', urllib.parse.quote_plus(domain), urllib.parse.quote_plus(identity), urllib.parse.quote_plus(selector) + '.txt', ) try: with open(subpath) as fh: - print(f'Loading WKD key from {subpath}') + logger.info('Loading: WKD key from %s', subpath) return fh.readline().strip() except IOError: pass # We use zbase32 because this is what OpenPGP uses - # I'm not convinced this is a sane choice + # I'm not convinced this is a sane choice, if only because + # it has a swear word to my account record. i = hashlib.sha1(identity.lower().encode()).digest() zdir = anybase32.encode(i, anybase32.ZBASE32).decode() wkurl = f'https://{domain}/.well-known/devkey/{zdir}/{selector}.txt' - print(f'Retrieving: {wkurl}') + logger.info('Retrieving: %s', wkurl) res = requests.get(wkurl, timeout=timeout) if res.status_code != 200: - print(f'Could not get {wkurl}') + logger.critical('Could not get %s: %s', wkurl, res.status_code) sys.exit(1) # For POC purposes, we are not doing caching or TOFU management, # but this would be a required part of actual implementation return res.content.decode().strip() -def get_b64_attestation(msg): +def get_b64_attestation(msg) -> Tuple[str, str, str]: # b4 stores these as hexdigests, but it's more natural # for email headers to use b64 encoded values, if only # to save a few bytes of space @@ -158,7 +215,7 @@ return hhdr -def get_parts_from_header(hstr): +def get_parts_from_header(hstr: str) -> dict: hstr = re.sub(r'\s*', '', hstr) hdata = dict() for chunk in hstr.split(';'): @@ -169,7 +226,7 @@ return hdata -def dkim_canonicalize_header(hname, hval): +def dkim_canonicalize_header(hname: str, hval: str) -> Tuple[str, str]: hname = hname.lower() hval = hval.strip() hval = re.sub(r'\n', '', hval) @@ -177,7 +234,7 @@ return hname, hval -def verify_attestation_hashes(msg): +def verify_attestation_hashes(msg) -> bool: hhdr = msg.get(XPH_HDR) hdata = get_parts_from_header(str(hhdr)) adata = dict() @@ -187,20 +244,92 @@ 'p': 'diff content', } adata['i'], adata['m'], adata['p'] = get_b64_attestation(msg) + verified = True + logger.info('----- ---------------') for part, what in desc.items(): if hdata[part] == adata[part]: status = 'PASS' else: + verified = False status = 'FAIL' - print(f'{status} | {desc[part]}') + logger.info('%s : %s', status, desc[part]) + logger.info('----- ---------------') + if verified: + logger.info('PASS : All hashes verified') + else: + logger.info('FAIL : Some or all hashes failed verification') + + return verified -def cmd_hashes_hdr(msg): +def gpg_verify(smsg: bytes, dsig: bytes, keyid: str) -> bool: + # We can't pass both the detached sig and the content on stdin, so + # use a temporary file + savefile = mkstemp('attpoc-pgp-verify')[1] + with open(savefile, 'wb') as fh: + fh.write(dsig) + vrfyargs = ['--verify', '--status-fd=1', savefile, '-'] + # Do we have this key in our default keyring? + pubring = None + ecode, out, err = gpg_run_command(['--list-key', keyid]) + if ecode > 0: + # See if it's in git itself + gittop = get_git_toplevel() + if gittop: + # Do we have that key in git? + # URLencode keyid to avoid path-based badness + if os.path.exists( + os.path.join(gittop, '.keys', 'openpgp', 'keys', urllib.parse.quote_plus(keyid) + '.asc')): + pubring = os.path.join(gittop, '.keys', 'openpgp', 'pubring.kbx') + if pubring and os.path.exists(pubring): + logger.info('Loading: in-git pubring: %s', pubring) + gpgargs = ['--no-default-keyring', '--keyring', pubring] + vrfyargs + else: + logger.critical('Unable to find key %s', keyid) + sys.exit(1) + else: + gpgargs = vrfyargs + ecode, out, err = gpg_run_command(gpgargs, stdin=smsg) + os.unlink(savefile) + if ecode > 0: + logger.critical('PGP signature failed to verify') + logger.critical(err.decode()) + return False + output = out.decode() + # We're looking for GOODSIG and VALIDSIG + # For the purposes of this POC, we're not doing the following, but normally would: + # - check UIDs to make sure that they match From: + # - check signature date for its drift from Date: + gs_matches = re.search(r'^\[GNUPG:] GOODSIG ([0-9A-F]+)\s+(.*)$', output, re.M) + vs_matches = re.search(r'^\[GNUPG:] VALIDSIG ([0-9A-F]+) (\d{4}-\d{2}-\d{2}) (\d+)', output, re.M) + if gs_matches and vs_matches: + signer = gs_matches.groups()[1] + if not pubring: + # If we're not using an in-git pubring, also check TRUST_* output + ts_matches = re.search(r'^\[GNUPG:] TRUST_(FULLY|ULTIMATE)', output, re.M) + if not ts_matches: + logger.critical('Insufficient TRUST on: %s', signer) + return False + + logger.info('PGP signature verified: %s', signer) + return True + + # Theoretically, this would have resulted in ecode > 0 + logger.critical('PGP signature failed to verify') + logger.critical(err.decode()) + return False + + +def cmd_hashes_hdr(cmdargs) -> None: + msg = load_message(cmdargs.message) hhdr = gen_hashes_header(msg) - print(hhdr.encode()) + logger.info('--- HEADER STARTS ---') + sys.stdout.buffer.write(XPH_HDR.encode() + b': ' + hhdr.encode().encode() + b'\r\n') -def cmd_dkim_sign(msg): +def cmd_sign_dkim(cmdargs) -> None: + logger.info('Signing: plain DKIM') + msg = load_message(cmdargs.message) # We use dkimpy here as a demonstration of an external DKIM compliant # implementation generating the DKIM-Signature tag, complete with bh= # body hash that we don't consider for our purposes: @@ -210,21 +339,25 @@ # whitespace, which allows sneaking in maliciously modified patches # in languages with syntactic whitespace. msg = add_hashes_header(msg) - selector = b'example' - domain = b'example.org' + domain = cmdargs.domain.encode() + selector = cmdargs.selector.encode() + identity = cmdargs.identity.encode() include_headers = [b'from', b'date', b'x-patch-hashes'] - with open('rsa.key', 'rb') as fh: + with open(cmdargs.privkey, 'rb') as fh: + logger.info('Using %s to sign', cmdargs.privkey) privkey = fh.read() dk = dkim.DKIM(msg.as_bytes()) - bhdr = dk.sign(selector, domain, privkey, include_headers=include_headers) + bhdr = dk.sign(selector, domain, privkey, identity=identity, include_headers=include_headers) dhdr = bhdr.decode() msg['DKIM-Signature'] = dhdr.split(':', 1)[1] - print(msg.as_string()) + logger.info('--- MESSAGE STARTS ---') + sys.stdout.buffer.write(msg.as_bytes()) -def cmd_dkim_verify(msg): +def cmd_verify_dkim(msg) -> None: + logger.info('Verifying: Plain DKIM') # We don't use dkimpy to verify, as it will force verification of bh=, which # we intentionally choose not to use for reasons listed above. # However, our implementation is simplistic and doesn't cover many aspects @@ -233,6 +366,11 @@ # bh= field. dks = msg.get('dkim-signature') ddata = get_parts_from_header(dks) + # Make sure the x-patch-hashes header is included + included = ddata['h'].split(':') + if XPH_HDR.lower() not in included: + logger.critical('%s is not included in signed headers, unable to verify!', XPH_HDR) + sys.exit(1) pk = base64.b64decode(get_dkim_key(ddata['d'], ddata['s'])) sig = base64.b64decode(ddata['b']) headers = list() @@ -256,13 +394,17 @@ # noinspection PyTypeChecker pkcs1_15.new(key).verify(hashed, sig) except (ValueError, TypeError): - print('The DKIM signature did NOT verify!') + logger.critical('FAIL: The DKIM signature did NOT verify!') sys.exit(1) - print(f'DKIM signature verified for d={ddata["i"]} s={ddata["s"]}') + if not verify_identity_domain(msg, ddata['i'], ddata['d']): + sys.exit(1) + logger.info('PASS : DKIM signature for d=%s, s=%s', ddata['d'], ddata['s']) verify_attestation_hashes(msg) -def cmd_pgp_sign(msg, identity='mricon@kernel.org', selector='E63EDCA9329DD07E'): +def cmd_sign_pgp(cmdargs) -> None: + logger.info('Signing: PGP') + msg = load_message(cmdargs.message) # selector is the key id of the [C] public key, e.g. 0xE63EDCA9329DD07E. # identity is the UID we should look for on the key # The signature can be made with a subkey, so we embed the key ID into @@ -270,33 +412,37 @@ # We don't embed signing time, as it's part of the PGP signature. headers = list() hhdr = gen_hashes_header(msg) + msg[XPH_HDR] = hhdr hhname, hhval = dkim_canonicalize_header(XPH_HDR, hhdr.encode()) headers.append(f'{hhname}:{hhval}') hparts = [ 'm=pgp', - f'i={identity}', - f's=0x{selector}', + f'i={cmdargs.identity}', + f's=0x{cmdargs.keyid}', 'b=', ] shname, shval = dkim_canonicalize_header(XPS_HDR, '; '.join(hparts)) headers.append(f'{shname}:{shval}') payload = '\r\n'.join(headers).encode() - gpgargs = ['-b', '-u', selector] + if cmdargs.subkeyid: + gpgargs = ['-b', '-u', f'{cmdargs.subkeyid}!'] + else: + gpgargs = ['-b', '-u', cmdargs.keyid] ecode, out, err = gpg_run_command(gpgargs, payload) if ecode > 0: - print('Running gpg failed') - print(err.decode()) + logger.critical('Running gpg failed') + logger.critical(err.decode()) sys.exit(ecode) bdata = base64.b64encode(out) - shval += bdata.decode() + shval += splitter(bdata.decode()) shdr = email.header.make_header([(shval.encode(), 'us-ascii')], maxlinelen=78) msg[XPS_HDR] = shdr - print(msg.as_string()) + logger.info('--- MESSAGE STARTS ---') + sys.stdout.buffer.write(msg.as_bytes()) def cmd_pgp_verify(msg): - # For the POC purposes, we ignore key lookup and assume it's in the keyring shdr = msg.get(XPS_HDR) sdata = get_parts_from_header(shdr) sig = base64.b64decode(sdata['b']) @@ -308,32 +454,14 @@ shval = shval.rsplit('; b=')[0] + '; b=' headers.append(f'{shname}:{shval}') payload = ('\r\n'.join(headers)).encode() - # We can't pass both the detached sig and the content on stdin, so - # use a temporary file - savefile = mkstemp('attpoc-pgp-verify')[1] - with open(savefile, 'wb') as fh: - fh.write(sig) - gpgargs = ['--verify', '--status-fd=1', savefile, '-'] - ecode, out, err = gpg_run_command(gpgargs, stdin=payload) - os.unlink(savefile) - if ecode > 0: - print('PGP signature failed to verify') - print(err.decode()) - sys.exit(1) - output = out.decode() - # We're looking for GOODSIG and VALIDSIG - # For the purposes of this POC, we're not doing the following, but normally would: - # - check UIDs to make sure that they match From: - # - check signature date to make sure its drift from Date: - # - check TRUST_ to make sure it's sufficiently trusted key - gs_matches = re.search(r'^\[GNUPG:] GOODSIG ([0-9A-F]+)\s+(.*)$', output, re.M) - vs_matches = re.search(r'^\[GNUPG:] VALIDSIG ([0-9A-F]+) (\d{4}-\d{2}-\d{2}) (\d+)', output, re.M) - if gs_matches and vs_matches: - print('PGP signature verified: %s' % gs_matches.groups()[1]) - verify_attestation_hashes(msg) + keyid = re.sub(r'^0x', '', sdata['s']) + if gpg_verify(payload, sig, keyid): + verify_attestation_hashes(msg) -def cmd_dk_sign(msg, domain='kernel.org', identity='@kernel.org', selector='patches', keyfile='dk.key', mode='dk'): +def cmd_sign_dk(cmdargs, mode='dk'): + logger.info('Signing: plain DKIM') + msg = load_message(cmdargs.message) # selector is the same as in DKIM, the leftmost part of foo._domainkey.example.org # identity should match domain in from headers = list() @@ -345,9 +473,9 @@ hparts = [ f'm={mode}', - f'd={domain}', - f'i={identity}', - f's={selector}', + f'd={cmdargs.domain}', + f'i={cmdargs.identity}', + f's={cmdargs.selector}', f't={signtime}', 'a=ed25519-sha256', 'b=', @@ -358,20 +486,22 @@ hashed = hashlib.sha256() hashed.update(payload) try: - with open(keyfile, 'r') as fh: + with open(cmdargs.privkey, 'r') as fh: sk = SigningKey(fh.read(), encoder=Base64Encoder) except IOError: - print('Could not open %s' % keyfile) + logger.critical('Could not open %s', cmdargs.privkey) sys.exit(1) bdata = sk.sign(hashed.digest(), encoder=Base64Encoder) - shval += bdata.decode() + shval += splitter(bdata.decode()) shdr = email.header.make_header([(shval.encode(), 'us-ascii')], maxlinelen=78) msg[XPS_HDR] = shdr - print(msg.as_string()) + logger.info('--- MESSAGE STARTS ---') + sys.stdout.buffer.write(msg.as_bytes()) -def cmd_dk_verify(msg, mode='dk'): +def cmd_verify_dk(msg, mode='dk'): + logger.info('Verifying: %s (mode=%s)', XPS_HDR, mode) shdr = msg.get(XPS_HDR) sdata = get_parts_from_header(shdr) headers = list() @@ -391,101 +521,153 @@ elif mode == 'wkd': pk = get_wkd_key(sdata['d'], sdata['i'], sdata['s']) else: - print(f'Unknown mode: {mode}') + logger.critical('Unknown mode: %s', mode) sys.exit(1) - mode = mode.upper() - vk = VerifyKey(pk, encoder=Base64Encoder) try: foo = vk.verify(sdata['b'].encode(), encoder=Base64Encoder) except BadSignatureError: - print(f'{mode} signature verification FAILED for: d={sdata["d"]}, i={sdata["i"]}, s={sdata["s"]}') + logger.critical('FAIL : mode=%s signature verification for: d=%s, i=%s, s=%s', mode, + sdata['d'], sdata['i'], sdata['s']) sys.exit(1) if foo != hashed.digest(): - print(f'{mode} signature verification FAILED for: d={sdata["d"]}, i={sdata["i"]}, s={sdata["s"]}') + logger.critical('FAIL : mode=%s signature verification for: d=%s, i=%s, s=%s', mode, + sdata['d'], sdata['i'], sdata['s']) sys.exit(1) - print(f'{mode} signature verified for: d={sdata["d"]}, i={sdata["i"]}, s={sdata["s"]}') + if not verify_identity_domain(msg, sdata['i'], sdata['d']): + sys.exit(1) + logger.info('PASS : mode=%s signature verified for: d=%s, i=%s, s=%s', mode, + sdata['d'], sdata['i'], sdata['s']) verify_attestation_hashes(msg) -def cmd_wk_sign(msg, domain='kernel.org', identity='@kernel.org', selector='patches', keyfile='dk.key'): - cmd_dk_sign(msg, domain=domain, identity=identity, selector=selector, keyfile=keyfile, mode='wk') +def cmd_sign_wk(cmdargs): + cmd_sign_dk(cmdargs, mode='wk') -def cmd_wk_verify(msg): - cmd_dk_verify(msg, mode='wk') +def cmd_verify_wk(msg): + cmd_verify_dk(msg, mode='wk') -def cmd_wkd_sign(msg, domain='kernel.org', identity='dev@kernel.org', selector='patches', keyfile='ingit.key'): - cmd_dk_sign(msg, domain=domain, identity=identity, selector=selector, keyfile=keyfile, mode='wkd') +def cmd_sign_wkd(cmdargs): + cmd_sign_dk(cmdargs, mode='wkd') -def cmd_wkd_verify(msg): - cmd_dk_verify(msg, mode='wkd') +def cmd_verify_wkd(msg): + cmd_verify_dk(msg, mode='wkd') -def cmd_verify(msg): +def cmd_verify(cmdargs): + msg = load_message(cmdargs.message) # do we have a hashes header? if not msg.get(XPH_HDR): - print(f'Message does not contain {XPH_HDR}, nothing to verify') + logger.critical('Message does not contain %s, nothing to verify', XPH_HDR) sys.exit(1) # do we have a sig header? shdr = msg.get(XPS_HDR) if not shdr: if not msg.get('dkim-signature'): - print(f'Message contains unsigned hashes, cannot verify') + logger.critical('Message contains unsigned hashes, cannot verify') sys.exit(1) # Run as a plain dkim verification - cmd_dkim_verify(msg) + cmd_verify_dkim(msg) sys.exit(0) sdata = get_parts_from_header(shdr) if sdata['m'] == 'pgp': cmd_pgp_verify(msg) sys.exit(0) if sdata['m'] == 'dk': - cmd_dk_verify(msg) + cmd_verify_dk(msg) sys.exit(0) if sdata['m'] == 'wk': - cmd_wk_verify(msg) + cmd_verify_wk(msg) sys.exit(0) if sdata['m'] == 'wkd': - cmd_wkd_verify(msg) + cmd_verify_wkd(msg) sys.exit(0) - print(f'Unknown mode: {sdata["m"]}') + logger.critical('Unknown mode: %s', sdata['m']) sys.exit(1) if __name__ == '__main__': - message = sys.stdin.buffer.read() - if not message: - print('Pass message on stdin') + import argparse + + # noinspection PyTypeChecker + parser = argparse.ArgumentParser( + prog='main', + description='A proof of concept tool for header-based email patch attestation', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('-m', '--message', default='emails/unsigned.eml', + help='File with the message to work with') + parser.add_argument('-v', '--verbose', default=False, + help='Print extra debugging output') + subparsers = parser.add_subparsers(help='sub-command help', dest='subcmd') + + # hashes-hdr + sp_hhdr = subparsers.add_parser('hashes-hdr', help='Create a hashes header') + sp_hhdr.set_defaults(func=cmd_hashes_hdr) + + # sign-dkim + sp_sdkim = subparsers.add_parser('sign-dkim', help='Create a DKIM signature') + sp_sdkim.add_argument('-i', '--identity', default='@example.org') + sp_sdkim.add_argument('-d', '--domain', default='example.org') + sp_sdkim.add_argument('-s', '--selector', default='patches') + sp_sdkim.add_argument('-k', '--privkey', default='rsa.key') + sp_sdkim.set_defaults(func=cmd_sign_dkim) + + # sign-dk + sp_sdk = subparsers.add_parser('sign-dk', help='Create a mode=dk signature') + sp_sdk.add_argument('-i', '--identity', default='@example.org') + sp_sdk.add_argument('-d', '--domain', default='example.org') + sp_sdk.add_argument('-s', '--selector', default='patches') + sp_sdk.add_argument('-k', '--privkey', default='dk.key') + sp_sdk.set_defaults(func=cmd_sign_dk) + + # sign-wk + sp_swk = subparsers.add_parser('sign-wk', help='Create a mode=wk signature') + sp_swk.add_argument('-i', '--identity', default='@example.org') + sp_swk.add_argument('-d', '--domain', default='example.org') + sp_swk.add_argument('-s', '--selector', default='patches') + sp_swk.add_argument('-k', '--privkey', default='dk.key') + sp_swk.set_defaults(func=cmd_sign_wk) + + # sign-wk + sp_swkd = subparsers.add_parser('sign-wkd', help='Create a mode=wkd signature') + sp_swkd.add_argument('-i', '--identity', default='dev@kernel.org') + sp_swkd.add_argument('-d', '--domain', default='example.org') + sp_swkd.add_argument('-s', '--selector', default='patches') + sp_swkd.add_argument('-k', '--privkey', default='ingit.key') + sp_swkd.set_defaults(func=cmd_sign_wkd) + + # sign-pgp + sp_spgp = subparsers.add_parser('sign-pgp', help='Create a PGP signature') + sp_spgp.add_argument('-i', '--identity', default='dev@kernel.org') + sp_spgp.add_argument('-k', '--keyid', default='AAAABBBBCCCCDDDD') + sp_spgp.add_argument('-u', '--subkeyid') + sp_spgp.set_defaults(func=cmd_sign_pgp) + + # verify + sp_verify = subparsers.add_parser('verify', help='Verify a signed message') + sp_verify.set_defaults(func=cmd_verify) + + args = parser.parse_args() + + logger.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + formatter = logging.Formatter('%(message)s') + ch.setFormatter(formatter) + if args.verbose: + ch.setLevel(logging.DEBUG) + else: + ch.setLevel(logging.INFO) + logger.addHandler(ch) + + if 'func' not in args: + parser.print_help() sys.exit(1) - orig_msg = email.message_from_bytes(message) - - commands = { - 'hashes-hdr': cmd_hashes_hdr, - 'dkim-sign': cmd_dkim_sign, - 'pgp-sign': cmd_pgp_sign, - 'dk-sign': cmd_dk_sign, - 'wk-sign': cmd_wk_sign, - 'wkd-sign': cmd_wkd_sign, - 'verify': cmd_verify, - } - - command = None - if len(sys.argv) > 1: - command = sys.argv[1] - - if command not in commands: - print(f'Unknown command: {command}') - print('Known commands:') - for goodcmd in commands.keys(): - print(f' {goodcmd}') - sys.exit(1) - - execfunc = commands[command] - execfunc(orig_msg) + args.func(args)
diff --git a/verify-all.sh b/verify-all.sh index 3e8595a..cbd3acf 100755 --- a/verify-all.sh +++ b/verify-all.sh
@@ -5,23 +5,38 @@ # source .venv/bin/activate # pip install -r requirements.txt echo "--------- UNHASHED (expected to fail) ---------" -./main.py verify < emails/unsigned.eml +./main.py -m emails/unsigned.eml verify +echo echo "--------- UNSIGNED (expected to fail) ---------" -./main.py verify < emails/unsigned-with-hashes.eml +./main.py -m emails/unsigned-with-hashes.eml verify +echo echo "--------- DKIM-SIGNED (all PASS) ---------" -./main.py verify < emails/korg-signed-dkim.eml -echo "--------- DKIM-SIGNED (with ML junk) ---------" -./main.py verify < emails/korg-signed-dkim-with-ml-junk.eml +./main.py -m emails/korg-signed-dkim.eml verify +echo +echo "--------- DKIM-SIGNED (with ML junk, all PASS) ---------" +./main.py -m emails/korg-signed-dkim-with-ml-junk.eml verify +echo echo "--------- DKIM-SIGNED (with edited subject, metadata will FAIL) ---------" -./main.py verify < emails/korg-signed-dkim-changed-subject.eml +./main.py -m emails/korg-signed-dkim-changed-subject.eml verify +echo echo "--------- DK-SIGNED (all PASS) ---------" -./main.py verify < emails/korg-signed-dk.eml +./main.py -m emails/korg-signed-dk.eml verify +echo echo "--------- WK-SIGNED (all PASS) ---------" -./main.py verify < emails/korg-signed-wk.eml +./main.py -m emails/korg-signed-wk.eml verify +echo echo "--------- PGP-SIGNED (by mricon, all PASS) ---------" -# This assumes you've imported mricon.asc into your keyring -./main.py verify < emails/mricon-signed-pgp.eml +./main.py -m emails/mricon-signed-pgp.eml verify +echo +echo "--------- PGP-SIGNED (by mricon, commit message will FAIL) ---------" +./main.py -m emails/mricon-signed-pgp-commit-message-modified.eml verify +echo +echo "--------- PGP-SIGNED (by mricon, diff content will FAIL) ---------" +./main.py -m emails/mricon-signed-pgp-patch-modified.eml verify +echo echo "--------- WKD-SIGNED (by mricon, web lookup, all PASS) ---------" -./main.py verify < emails/mricon-signed-wkd.eml +./main.py -m emails/mricon-signed-wkd.eml verify +echo echo "--------- WKD-SIGNED (by dev, stored in git, all PASS) ---------" -./main.py verify < emails/dev-signed-wkd-ingit.eml +./main.py -m emails/dev-signed-wkd-ingit.eml verify +echo