From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-18.4 required=3.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,INCLUDES_CR_TRAILER,INCLUDES_PATCH, SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1EF77C47076 for ; Fri, 21 May 2021 18:48:13 +0000 (UTC) Received: by mail.kernel.org (Postfix) id E63B3611AD; Fri, 21 May 2021 18:48:12 +0000 (UTC) Received: by mail.kernel.org (Postfix) with ESMTPSA id AA69B601FD for ; Fri, 21 May 2021 18:48:12 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=linuxfoundation.org; s=korg; t=1621622892; bh=3fHX5/IJk9dzdn9TWImZG6DlVL0kWx885UpaKHsweos=; h=From:List-Id:To:Subject:Date:From; b=VzNiVQHQfuIDh2Z/ck7mFSuor8V37+FYIhXfLhn1hch2xFwnaY/tQEW0nvONT6VyT dIggMU+gWSIZmNPx/BVAD2Uc2Dlh9J9mc3jjLx6kB2IBxmaMqA5fnEQOVLDL6/A+xN KBCfiXVfLhbNJarnLtvFY4LTc3hPqYfiiOTU/SSc= From: Konstantin Ryabitsev List-Id: To: signatures@kernel.org Subject: [PATCH 1/3] Sign Message-Id header if present Date: Fri, 21 May 2021 14:48:09 -0400 Message-Id: <20210521184811.617875-1-konstantin@linuxfoundation.org> X-Mailer: git-send-email 2.31.1 MIME-Version: 1.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=6004; h=from:subject; bh=3fHX5/IJk9dzdn9TWImZG6DlVL0kWx885UpaKHsweos=; b=owGbwMvMwCG27YjM47CUmTmMp9WSGBJWMGS/ffMg+3mT2oo3V4W+/A0SCqlZm90mtDlYnGvZtQ4h 60XcHaUsDGIcDLJiiixl+2I3BRU+9JBL7zGFmcPKBDKEgYtTACay24Thn4rHlfankgo9937c2Mqa0r Rk2+v02/HpihXfHzmf0GXNVmFkaJn1+H5fmf7huH7T1Xsnx6uJtO8InKSRaTixQHHpzPdxXAA= X-Developer-Key: i=konstantin@linuxfoundation.org; a=openpgp; fpr=DE0E66E32F1FDD0902666B96E63EDCA9329DD07E Content-Transfer-Encoding: 8bit It is useful to sign the message-id header, because it is frequently used as the patch identifier. Unfortunately, unless git-format-patch is run with --thread, the message-id won't be generated until *after* the sendemail-validate hook is invoked, so most of the time we won't end up signing that header. However, having this as an option is handy. Signed-off-by: Konstantin Ryabitsev --- man/patatt.5 | 2 +- man/patatt.5.rst | 4 +-- patatt/__init__.py | 68 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/man/patatt.5 b/man/patatt.5 index 70cea05..1e46a8a 100644 --- a/man/patatt.5 +++ b/man/patatt.5 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH PATATT 5 "2021-05-11" "0.2.0" "" +.TH PATATT 5 "2021-05-21" "0.4.0" "" .SH NAME PATATT \- DKIM-like cryptographic patch attestation . diff --git a/man/patatt.5.rst b/man/patatt.5.rst index 2ab345c..845595b 100644 --- a/man/patatt.5.rst +++ b/man/patatt.5.rst @@ -5,10 +5,10 @@ DKIM-like cryptographic patch attestation ----------------------------------------- :Author: mricon@kernel.org -:Date: 2021-05-11 +:Date: 2021-05-21 :Copyright: The Linux Foundation and contributors :License: MIT-0 -:Version: 0.2.0 +:Version: 0.4.0 :Manual section: 5 SYNOPSIS diff --git a/patatt/__init__.py b/patatt/__init__.py index 92dee85..76028ae 100644 --- a/patatt/__init__.py +++ b/patatt/__init__.py @@ -41,12 +41,13 @@ RES_ERROR = 16 RES_BADSIG = 32 REQ_HDRS = [b'from', b'subject'] +OPT_HDRS = [b'message-id'] # Quick cache for key info KEYCACHE = dict() # My version -__VERSION__ = '0.3.0' +__VERSION__ = '0.4.0-dev' MAX_SUPPORTED_FORMAT_VERSION = 1 @@ -126,29 +127,56 @@ class DevsigHeader: self._body_hash = base64.b64encode(hashed.digest()) # do any git-mailinfo normalization prior to calling this - def set_headers(self, headers: list) -> None: - hfield = self.get_field('h') - if hfield: - # Make sure REQ_HEADERS are in this list - want_headers = [x.strip() for x in hfield.split(b':')] - for rqhdr in REQ_HDRS: - if rqhdr not in want_headers: - raise ValidationError('Signature is missing a required header %s' % rqhdr.decode()) - else: - want_headers = REQ_HDRS - - self._headervals = list() - for header in headers: + def set_headers(self, headers: list, mode: str) -> None: + parsed = list() + allhdrs = set() + # DKIM operates on headers in reverse order + for header in reversed(headers): try: left, right = header.split(b':', 1) hname = left.strip().lower() - if hname not in want_headers: - continue + parsed.append((hname, right)) + allhdrs.add(hname) except ValueError: continue - self._headervals.append(hname + b':' + DevsigHeader._dkim_canonicalize_header(right)) - self.hdata['h'] = b':'.join(want_headers) + reqset = set(REQ_HDRS) + optset = set(OPT_HDRS) + self._headervals = list() + if mode == 'sign': + # Make sure REQ_HDRS is a subset of allhdrs + if not reqset.issubset(allhdrs): + raise SigningError('The following required headers not present: %s' + % (b', '.join(reqset.difference(allhdrs)).decode())) + # Add optional headers that are actually present + optpresent = allhdrs.intersection(optset) + signlist = list(reqset.union(optpresent)) + self.hdata['h'] = b':'.join(signlist) + + elif mode == 'validate': + hfield = self.get_field('h') + signlist = [x.strip() for x in hfield.split(b':')] + # Make sure REQ_HEADERS are in this set + if not reqset.issubset(set(signlist)): + raise ValidationError('The following required headers not signed: %s' + % (b', '.join(reqset.difference(set(signlist))).decode())) + else: + raise RuntimeError('Unknown set_header mode: %s' % mode) + + for shname in signlist: + if shname not in allhdrs: + # Per RFC: + # Nonexistent header fields do not contribute to the signature computation (that is, they are + # treated as the null input, including the header field name, the separating colon, the header field + # value, and any CRLF terminator). + continue + at = 0 + for hname, rawval in list(parsed): + if hname == shname: + self._headervals.append(hname + b':' + DevsigHeader._dkim_canonicalize_header(rawval)) + parsed.pop(at) + break + at += 1 def sanity_check(self) -> None: if 'a' not in self.hdata: @@ -435,7 +463,7 @@ class PatattMessage: self.headers.remove(header) self.git_canonicalize() ds = DevsigHeader() - ds.set_headers(self.canon_headers) + ds.set_headers(self.canon_headers, mode='sign') ds.set_body(self.canon_body) ds.set_field('l', str(len(self.canon_body))) if identity and identity != self.canon_identity: @@ -478,7 +506,7 @@ class PatattMessage: raise ValidationError('No signatures matching identity %s' % identity) self.git_canonicalize() - vds.set_headers(self.canon_headers) + vds.set_headers(self.canon_headers, mode='validate') if trim_body: lfield = vds.get_field('l') -- 2.31.1