Subject: 30.1; mml: Produce Unobtrusive Signatures

From: Daniel Kahn Gillmor <[email-masked]>
Date: Thu, 15 May 2025 23:56:25 -0400

Package: emacs,gnus
Version: 30.1

I'm running emacs 30.1 on debian.  When sending cleartext mail, i want
to be able to produce an "Unobtrusive Signature" using OpenPGP as
described in
https://datatracker.ietf.org/doc/draft-gallagher-email-unobtrusive-signatures/

Those kinds of signatures provide end-to-end cryptographic protection
for headers (based on
https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/ )
as well as the message body, and are less likely to inflict bad UX or
bad rendering on recipients using legacy MUAs compared to PGP/MIME (or
inline PGP, for that matter).

These three patches appear to do the trick for me.  I'm not an elisp
guru or a gnus expert.  I'm happy to hear any feedback about how they
could be improved.

Regards,

        --dkg

From 81c3e7f5de0bbb24bbd2a2b43a103fb83c530f6d Mon Sep 17 00:00:00 2001
From: Daniel Kahn Gillmor <[email-masked]>
Date: Thu, 15 May 2025 21:49:32 -0400
Subject: [PATCH 1/3] mml: Pass likely headers through to mml-sec functions

By pre-computing the likely headers for an outbound message, and passing
them along as a tag in mml-parse, we create an opportunity to provide
Header Protection as described in
https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/

Signed-off-by: Daniel Kahn Gillmor <[email-masked]>
---
 lisp/gnus/mml.el | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/lisp/gnus/mml.el b/lisp/gnus/mml.el
index 51d8d2c3769..11a8de7c011 100644
--- a/lisp/gnus/mml.el
+++ b/lisp/gnus/mml.el
@@ -265,6 +265,8 @@ part.  This is for the internal use, you should never modify the value.")
 	  (apply #'mml-insert-tag
 		 secure-mode
 		 `(,@tags
+                   ,"likely-headers"
+                   ,(mml-get-likely-headers)
 		   ,(if keyfile "keyfile")
 		   ,keyfile
 		   ,@(apply #'append
@@ -492,6 +494,21 @@ If MML is non-nil, return the buffer up till the correspondent mml tag."
 (declare-function libxml-parse-html-region "xml.c"
 		  (start end &optional base-url discard-comments))
 
+(defun mml-get-likely-headers ()
+  "Get likely final headers from the existing message"
+  (save-excursion
+    (save-restriction
+      (message-narrow-to-headers-or-head)
+      (let ((x (buffer-substring (point-min) (point-max))))
+        (with-temp-buffer
+          (insert x)
+          (message-remove-header "Bcc")
+          (message-remove-header message-ignored-mail-headers t)
+          (mail-encode-encoded-word-buffer)
+          (message-cleanup-headers)
+          (buffer-string)
+          )))))
+
 (defun mml-generate-mime (&optional multipart-type content-type)
   "Generate a MIME message based on the current MML document.
 MULTIPART-TYPE defaults to \"mixed\", but can also
-- 
2.47.2

From 75f8c5c936deafea1ee44edad5e0f530ec6c4dfc Mon Sep 17 00:00:00 2001
From: Daniel Kahn Gillmor <[email-masked]>
Date: Thu, 15 May 2025 21:54:06 -0400
Subject: [PATCH 2/3] mml: Enable production of Unobtrusive Signatures via epg

https://datatracker.ietf.org/doc/draft-gallagher-email-invisible-signatures/
describes a mechanism to produce cleartext signatures over MIME messages
that are less likely to cause problems than traditional PGP/MIME.

With this patch, it's possible to produce those signatures with:

   (mml-secure-message "unobtrusive" 'sign)

This patch only works with epg, not with mailcrypt or pgg, because epg
is what i'm familiar with and what i can easily test.

Signed-off-by: Daniel Kahn Gillmor <[email-masked]>
---
 lisp/gnus/mml-sec.el |  6 ++++++
 lisp/gnus/mml2015.el | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/lisp/gnus/mml-sec.el b/lisp/gnus/mml-sec.el
index 8dffcf872f3..6fb82836e9a 100644
--- a/lisp/gnus/mml-sec.el
+++ b/lisp/gnus/mml-sec.el
@@ -34,6 +34,7 @@
 (autoload 'mail-strip-quoted-names "mail-utils")
 (autoload 'mml2015-sign "mml2015")
 (autoload 'mml2015-encrypt "mml2015")
+(autoload 'mml-unobtrusive-sign "mml2015")
 (autoload 'mml1991-sign "mml1991")
 (autoload 'mml1991-encrypt "mml1991")
 (autoload 'message-fetch-field "message")
@@ -56,6 +57,7 @@
   '(("smime"     mml-smime-sign-buffer     mml-smime-sign-query)
     ("pgp"       mml-pgp-sign-buffer       list)
     ("pgpauto"   mml-pgpauto-sign-buffer  list)
+    ("unobtrusive" mml-unobtrusive-sign-buffer list)
     ("pgpmime"   mml-pgpmime-sign-buffer   list))
   "Alist of MIME signer functions.")
 
@@ -198,6 +200,10 @@ You can also customize or set `mml-signencrypt-style-alist' instead."
   (or (mml2015-sign cont)
       (error "Signing failed... inspect message logs for errors")))
 
+(defun mml-unobtrusive-sign-buffer (cont)
+  (or (mml-unobtrusive-sign cont)
+      (error "Signing failed... inspect message logs for errors")))
+
 (defun mml-pgpmime-encrypt-buffer (cont &optional sign)
   (or (mml2015-encrypt cont sign)
       (error "Encryption failed... inspect message logs for errors")))
diff --git a/lisp/gnus/mml2015.el b/lisp/gnus/mml2015.el
index a46aa68f56a..646fb018a31 100644
--- a/lisp/gnus/mml2015.el
+++ b/lisp/gnus/mml2015.el
@@ -25,6 +25,9 @@
 ;; RFC 2015 is updated by RFC 3156, this file should be compatible
 ;; with both.
 
+;; This is also capable of producing unobtrusive signatures based on
+;; draft-gallagher-email-unobtrusive-signatures
+
 ;;; Code:
 
 (eval-when-compile (require 'cl-lib))
@@ -945,6 +948,42 @@ If set, it overrides the setting of `mml2015-sign-with-sender'."
     (insert (format "--%s--\n" boundary))
     (goto-char (point-max))))
 
+;;; Unobtrusive Signatures, see:
+;;; https://datatracker.ietf.org/doc/draft-gallagher-email-unobtrusive-signatures/
+
+; convert ASCII-armored PGP SIGNATURE block to base64-encoded with FWS
+; at the start of each line:
+(defun pgpsig-armor-to-wrapped-b64 (s)
+  (string-join
+   (string-split
+    (string-trim-right
+     (string-trim-left s "-----BEGIN PGP SIGNATURE-----\n\\(?:[^\n]+\n\\)*\n")
+     "\n\\(?:=....\n\\)?-----END PGP SIGNATURE-----\n?")
+    "\n")
+   "\n "))
+
+(defun mml-unobtrusive-sign (cont)
+  (goto-char (point-min))
+  (insert (cdr (assq 'likely-headers cont)))
+  (re-search-forward "^Content-Type: [^\n]*\\(\n[ \t][^\n]*$\\)*")
+  (insert "; hp=\"clear\"")
+  (re-search-forward "^")
+  (let* ((pair (mml-secure-epg-sign 'OpenPGP t))
+	 (signature (car pair)))
+    (unless (stringp signature)
+      (error "Signature failed"))
+    (goto-char (point-min))
+    (insert (format "Sig: t=p; b=%s\n"
+                    (pgpsig-armor-to-wrapped-b64 signature)))
+    (let ((boundary (mml-compute-boundary cont)))
+      (goto-char (point-min))
+      (insert (format "Content-Type: multipart/mixed; boundary=\"%s\";\n"
+		      boundary))
+      (insert (format "\n--%s\n" boundary))
+      (goto-char (point-max))
+      (insert (format "\n--%s--\n" boundary))
+      (goto-char (point-max)))))
+
 ;;; General wrapper
 
 (autoload 'gnus-buffer-live-p "gnus-util")
-- 
2.47.2

From f03c16ddedaa0b7a39692629ab61e7c3b3e06201 Mon Sep 17 00:00:00 2001
From: Daniel Kahn Gillmor <[email-masked]>
Date: Thu, 15 May 2025 22:03:25 -0400
Subject: [PATCH 3/3] mml: Add C-c RET s u to make Unobtrusive Signature

This adds to the default keymap to make it relatively easy to make an
Unobtrusive Signature when sending mail.

Signed-off-by: Daniel Kahn Gillmor <[email-masked]>
---
 lisp/gnus/mml-sec.el | 5 +++++
 lisp/gnus/mml.el     | 6 ++++--
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/lisp/gnus/mml-sec.el b/lisp/gnus/mml-sec.el
index 6fb82836e9a..9b703abd5f4 100644
--- a/lisp/gnus/mml-sec.el
+++ b/lisp/gnus/mml-sec.el
@@ -285,6 +285,11 @@ Use METHOD if given.  Else use `mml-secure-method' or
   (interactive nil mml-mode)
   (mml-secure-part "pgpmime" 'sign))
 
+(defun mml-secure-sign-unobtrusive ()
+  "Add MML tags to unobtrusively sign this MML part."
+  (interactive nil mml-mode)
+  (mml-secure-part "unobtrusive" 'sign))
+
 (defun mml-secure-sign-smime ()
   "Add MML tags to S/MIME sign this MML part."
   (interactive nil mml-mode)
diff --git a/lisp/gnus/mml.el b/lisp/gnus/mml.el
index 11a8de7c011..7d849335154 100644
--- a/lisp/gnus/mml.el
+++ b/lisp/gnus/mml.el
@@ -1178,11 +1178,13 @@ If HANDLES is non-nil, use it instead reparsing the buffer."
     "s" (define-keymap
           "p" #'mml-secure-message-sign-pgpmime
           "o" #'mml-secure-message-sign-pgp
-          "s" #'mml-secure-message-sign-smime)
+          "s" #'mml-secure-message-sign-smime
+          "u" #'mml-secure-message-sign-unobtrusive)
     "S" (define-keymap
           "p" #'mml-secure-sign-pgpmime
           "o" #'mml-secure-sign-pgp
-          "s" #'mml-secure-sign-smime)
+          "s" #'mml-secure-sign-smime
+          "u" #'mml-secure-message-sign-unobtrusive)
     "c" (define-keymap
           "p" #'mml-secure-message-encrypt-pgpmime
           "o" #'mml-secure-message-encrypt-pgp
-- 
2.47.2