aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKonstantin Ryabitsev <konstantin@linuxfoundation.org>2021-05-21 11:39:38 -0400
committerKonstantin Ryabitsev <konstantin@linuxfoundation.org>2021-05-21 11:39:38 -0400
commite6fbfe8cafa3da6e9a2791325a633d4bab0c7460 (patch)
tree919a7defba891cdf5e3a787cdd2a020ffd70f79a
parent66a94f773a8d80ee5e3c1e2e747434c622047e2e (diff)
downloadpatatt-e6fbfe8cafa3da6e9a2791325a633d4bab0c7460.tar.gz
Sign Message-Id header if present
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 <konstantin@linuxfoundation.org>
-rw-r--r--man/patatt.52
-rw-r--r--man/patatt.5.rst4
-rw-r--r--patatt/__init__.py68
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')