[PATCH] Sign Message-Id header if present
Konstantin Ryabitsev <[email protected]> Fri, 21 May 2021 11:43:21 -0400
| Newsgroups | org.kernel.lore.signatures |
|---|---|
| Message-ID | <e6fbfe8cafa3da6e9a2791325a633d4bab0c7460.1621611731.git.konstantin@linuxfoundation.org> |
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 <[email protected]> --- 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: [email protected] -: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