* [PATCH] Sign Message-Id header if present
@ 2021-05-21 15:43 Konstantin Ryabitsev
0 siblings, 0 replies; only message in thread
From: Konstantin Ryabitsev @ 2021-05-21 15:43 UTC (permalink / raw)
To: signatures
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>
---
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
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2021-05-21 15:43 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-05-21 15:43 [PATCH] Sign Message-Id header if present Konstantin Ryabitsev
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).