From 7b8fbe2a3d7c454c030cabdf5de65b84c3383a7f Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Mon, 4 Mar 2024 23:27:00 +0100 Subject: gpg: fix mime-version header position Some MTAs try to normalize the case of all headers (including signed text parts headers). Unfortunately, Mime-Version can be normalized to different casing depending on the implementation (MIME- vs Mime-). Since the signature is computed on the whole part, including its header, changing the case can cause the signature to become invalid. Due to how multipart/signed messages are constructed, we need to hack around go-message writers to intercept the writing of a text part, compute its signature and write the actual message with the proper headers. Unfortunately, go-message does not allow creating a message writer that does not insert a Mime-Version header. This causes the Mime-Version header to be inserted in the wrong place: it is put inside the signed text part header instead on the top level header. Thus, included in the signed content. Make sure to remove any Mime-Version header from the signed part header. Finally, ensure that Mime-Version is set on the top-level header so that messages are compliant with RFC 2045. Fixes: https://todo.sr.ht/~rjarry/aerc/143 Link: https://github.com/emersion/go-message/issues/165 Link: https://github.com/emersion/go-pgpmail/pull/15 Link: https://lists.sr.ht/~rjarry/aerc-devel/%3CCQRPF5EA0TF8.PEJ4AKCEGMFM%40fembook%3E Changelog-fixed: `Mime-Version` is no longer inserted in signed text parts headers. MTAs normalizing header case will not corrupt signatures anymore. Reported-by: Coco Liliace Reported-by: Kirill Chibisov Signed-off-by: Robin Jarry Tested-by: Tim Culverhouse --- lib/crypto/gpg/writer.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/crypto/gpg/writer.go b/lib/crypto/gpg/writer.go index 0648ec24..caae56bf 100644 --- a/lib/crypto/gpg/writer.go +++ b/lib/crypto/gpg/writer.go @@ -10,6 +10,7 @@ import ( "mime" "git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin" + "github.com/emersion/go-message" "github.com/emersion/go-message/textproto" ) @@ -50,9 +51,26 @@ func (s *Signer) Write(p []byte) (int, error) { } func (s *Signer) Close() (err error) { - // TODO should write the whole message up here so we can get the proper micalg from the signature packet + msg, err := message.Read(&s.signedMsg) + if err != nil { + return err + } + // Make sure that MIME-Version is *not* set on the signed part header. + // It must be set *only* on the top level header. + // + // Some MTAs actually normalize the case of all headers (including + // signed text parts). MIME-Version can be normalized to different + // casing depending on the implementation (MIME- vs Mime-). + // + // Since the signature is computed on the whole part, including its + // header, changing the case can cause the signature to become invalid. + msg.Header.Del("Mime-Version") + + var buf bytes.Buffer + _ = textproto.WriteHeader(&buf, msg.Header.Header) + _, _ = io.Copy(&buf, msg.Body) - sig, micalg, err := gpgbin.Sign(bytes.NewReader(s.signedMsg.Bytes()), s.from) + sig, micalg, err := gpgbin.Sign(bytes.NewReader(buf.Bytes()), s.from) if err != nil { return err } @@ -62,13 +80,16 @@ func (s *Signer) Close() (err error) { "micalg": micalg, } s.header.Set("Content-Type", mime.FormatMediaType("multipart/signed", params)) + // Ensure Mime-Version header is set on the top level to be compliant + // with RFC 2045 + s.header.Set("Mime-Version", "1.0") if err = textproto.WriteHeader(s.w, s.header); err != nil { return err } boundary := s.mw.Boundary() fmt.Fprintf(s.w, "--%s\r\n", boundary) - _, _ = s.w.Write(s.signedMsg.Bytes()) + _, _ = s.w.Write(buf.Bytes()) _, _ = s.w.Write([]byte("\r\n")) var signedHeader textproto.Header @@ -114,6 +135,9 @@ func Encrypt(w io.Writer, h textproto.Header, rcpts []string, from string) (io.W "protocol": "application/pgp-encrypted", } h.Set("Content-Type", mime.FormatMediaType("multipart/encrypted", params)) + // Ensure Mime-Version header is set on the top level to be compliant + // with RFC 2045 + h.Set("Mime-Version", "1.0") if err := textproto.WriteHeader(w, h); err != nil { return nil, err -- cgit