[PATCH GnuPG] gpg: Verify Text mode Signatures over binary Literal Data Packets

Daniel Kahn Gillmor dkg at fifthhorseman.net
Wed Mar 5 16:19:36 CET 2025


* tests/openpgp/issue7539{.scm,-signer.asc,message.asc}: Add test
with text-mode Signature over binary Literal Data Packet.
* tests/openpgp/Makefile.am: include 7539 test materials.
* g10/filter.h: add textmode and seen_cr members to
md_filter_context_t
* g10/mainproc.c (proc_plaintext): set mfx.textmode if the signature
is over a text document.
* g10/mdfilter.c (md_filter): when mfx.textmode is set, normalize line
endings to CRLF. (free_md_filter_context): clean up textmode and
seen_cr
* g10/plaintext.c (handle_plaintext): when mfx.textmode is set,
normalize line endings to CRLF.

--

GnuPG does not produce text signatures over binary literal data
packets, but the OpenPGP standard permits them, and other OpenPGP
implementations may produce them.

See the discussion starting at
https://mailarchive.ietf.org/arch/msg/openpgp/RLMBugGhg_c9xT7zmDrkBklRZB8/

This fix still doesn't handle the even-more-obscure struture where
there are both text and binary signatures over a binary literal data
packet, like so:

  OPS0 OPS1 LITb SIG1 SIG0

But a more complete fix would be more complex (it would require, for
instance, multiple message digest handles), and this initial fix is
still a substantial improvement over the status quo.

GnuPG-bug-id: 7539
Signed-off-by: Daniel Kahn Gillmor <dkg at fifthhorseman.net>
---
 g10/filter.h                        |  2 ++
 g10/mainproc.c                      | 20 +++++++++++++++-----
 g10/mdfilter.c                      | 15 +++++++++++++--
 g10/plaintext.c                     | 28 ++++++++++++++++++++++++++--
 tests/openpgp/Makefile.am           |  7 +++++--
 tests/openpgp/issue7539-message.asc |  8 ++++++++
 tests/openpgp/issue7539-signer.asc  |  9 +++++++++
 tests/openpgp/issue7539.scm         | 29 +++++++++++++++++++++++++++++
 8 files changed, 107 insertions(+), 11 deletions(-)
 create mode 100644 tests/openpgp/issue7539-message.asc
 create mode 100644 tests/openpgp/issue7539-signer.asc
 create mode 100644 tests/openpgp/issue7539.scm

diff --git a/g10/filter.h b/g10/filter.h
index b15ce6aa5..c066898b2 100644
--- a/g10/filter.h
+++ b/g10/filter.h
@@ -27,6 +27,8 @@ typedef struct {
     gcry_md_hd_t md;      /* catch all */
     gcry_md_hd_t md2;     /* if we want to calculate an alternate hash */
     size_t maxbuf_size;
+    int textmode;         /* 1 hashing needs to normalize line-endings to CRLF */
+    int seen_cr;          /* 1 if last octet hashed was '\r' */
 } md_filter_context_t;
 
 typedef struct md_thd_filter_context *md_thd_filter_context_t;
diff --git a/g10/mainproc.c b/g10/mainproc.c
index ebbe4a6a7..ad615ff2c 100644
--- a/g10/mainproc.c
+++ b/g10/mainproc.c
@@ -993,9 +993,12 @@ proc_plaintext( CTX c, PACKET *pkt )
           if (n->pkt->pkt.onepass_sig->digest_algo)
             {
               if (!opt.skip_verify)
-                gcry_md_enable (c->mfx.md,
-                                n->pkt->pkt.onepass_sig->digest_algo);
-
+                {
+                  gcry_md_enable (c->mfx.md,
+                                  n->pkt->pkt.onepass_sig->digest_algo);
+                  if (n->pkt->pkt.onepass_sig->sig_class == 0x01)
+                    c->mfx.textmode = 1;
+                }
               any = 1;
             }
         }
@@ -1014,7 +1017,10 @@ proc_plaintext( CTX c, PACKET *pkt )
           clearsig = (*data == 0x01);
           for (data++, datalen--; datalen; datalen--, data++)
             if (!opt.skip_verify)
-              gcry_md_enable (c->mfx.md, *data);
+              {
+                gcry_md_enable (c->mfx.md, *data);
+                c->mfx.textmode = 1;
+              }
           any = 1;
           break;  /* Stop here as one-pass signature packets are not
                      expected.  */
@@ -1023,7 +1029,11 @@ proc_plaintext( CTX c, PACKET *pkt )
         {
           /* The SIG+LITERAL case that PGP used to use.  */
           if (!opt.skip_verify)
-            gcry_md_enable (c->mfx.md, n->pkt->pkt.signature->digest_algo);
+            {
+              gcry_md_enable (c->mfx.md, n->pkt->pkt.signature->digest_algo);
+              if (n->pkt->pkt.signature->sig_class == 0x01)
+                c->mfx.textmode = 1;
+            }
           any = 1;
         }
     }
diff --git a/g10/mdfilter.c b/g10/mdfilter.c
index a655d6d72..656661e49 100644
--- a/g10/mdfilter.c
+++ b/g10/mdfilter.c
@@ -41,7 +41,7 @@ md_filter( void *opaque, int control,
 {
     size_t size = *ret_len;
     md_filter_context_t *mfx = opaque;
-    int i, rc=0;
+    int i, rc=0, n;
 
     if( control == IOBUFCTRL_UNDERFLOW ) {
 	if( mfx->maxbuf_size && size > mfx->maxbuf_size )
@@ -49,7 +49,16 @@ md_filter( void *opaque, int control,
 	i = iobuf_read( a, buf, size );
 	if( i == -1 ) i = 0;
 	if( i ) {
-	    gcry_md_write(mfx->md, buf, i );
+            if (!mfx->textmode)
+              gcry_md_write(mfx->md, buf, i );
+            else
+              for (n = 0; n < i; n++)
+                {
+                  if (buf[n] == '\n' && !mfx->seen_cr)
+                    gcry_md_putc(mfx->md, '\r');
+                  gcry_md_putc(mfx->md, buf[n]);
+                  mfx->seen_cr = (buf[n] == '\r');
+                }
 	    if( mfx->md2 )
 		gcry_md_write(mfx->md2, buf, i );
 	}
@@ -71,6 +80,8 @@ free_md_filter_context( md_filter_context_t *mfx )
     mfx->md = NULL;
     mfx->md2 = NULL;
     mfx->maxbuf_size = 0;
+    mfx->textmode = 0;
+    mfx->seen_cr = 0;
 }
 
 
diff --git a/g10/plaintext.c b/g10/plaintext.c
index a96214994..8c2d2fcca 100644
--- a/g10/plaintext.c
+++ b/g10/plaintext.c
@@ -304,6 +304,7 @@ handle_plaintext (PKT_plaintext * pt, md_filter_context_t * mfx,
 	  while (pt->len)
 	    {
 	      int len = pt->len > temp_size ? temp_size : pt->len;
+              int n;
 	      len = iobuf_read (pt->buf, buffer, len);
 	      if (len == -1)
 		{
@@ -314,7 +315,18 @@ handle_plaintext (PKT_plaintext * pt, md_filter_context_t * mfx,
 		  goto leave;
 		}
 	      if (mfx->md)
-		gcry_md_write (mfx->md, buffer, len);
+                {
+                  if (!mfx->textmode)
+                    gcry_md_write (mfx->md, buffer, len);
+                  else
+                    for (n = 0; n < len; n++)
+                      {
+                        if (buffer[n] == '\n' && !mfx->seen_cr)
+                          gcry_md_putc(mfx->md, '\r');
+                        gcry_md_putc(mfx->md, buffer[n]);
+                        mfx->seen_cr = (buffer[n] == '\r');
+                      }
+                }
 	      if (fp)
 		{
 		  if (opt.max_output && (count += len) > opt.max_output)
@@ -402,12 +414,24 @@ handle_plaintext (PKT_plaintext * pt, md_filter_context_t * mfx,
 	       * So, always assume EOF if iobuf_read returns less bytes
 	       * then requested */
 	      int len = iobuf_read (pt->buf, buffer, temp_size);
+              int n;
 	      if (len == -1)
 		break;
 	      if (len < temp_size)
 		eof_seen = 1;
 	      if (mfx->md)
-		gcry_md_write (mfx->md, buffer, len);
+                {
+                  if (!mfx->textmode)
+                    gcry_md_write (mfx->md, buffer, len);
+                  else
+                    for (n = 0; n < len; n++)
+                      {
+                        if (buffer[n] == '\n' && !mfx->seen_cr)
+                          gcry_md_putc(mfx->md, '\r');
+                        gcry_md_putc(mfx->md, buffer[n]);
+                        mfx->seen_cr = (buffer[n] == '\r');
+                      }
+                }
 	      if (fp)
 		{
 		  if (opt.max_output && (count += len) > opt.max_output)
diff --git a/tests/openpgp/Makefile.am b/tests/openpgp/Makefile.am
index d1f04e99b..a3c82bad8 100644
--- a/tests/openpgp/Makefile.am
+++ b/tests/openpgp/Makefile.am
@@ -104,7 +104,8 @@ XTESTS = \
 	issue2417.scm \
 	issue2419.scm \
 	issue2929.scm \
-	issue2941.scm
+	issue2941.scm \
+	issue7539.scm
 
 
 # XXX: Currently, one cannot override automake's 'check' target.  As a
@@ -177,7 +178,9 @@ TEST_FILES = pubring.asc secring.asc plain-1o.asc plain-2o.asc plain-3o.asc \
 	     trust-pgp/david.sec.asc \
 	     trust-pgp/frank.sec.asc \
 	     trust-pgp/grace.sec.asc \
-	     trust-pgp/heidi.sec.asc
+	     trust-pgp/heidi.sec.asc \
+	     issue7539-signer.asc \
+	     issue7539-message.asc
 
 data_files = data-500 data-9000 data-32000 data-80000 plain-large
 
diff --git a/tests/openpgp/issue7539-message.asc b/tests/openpgp/issue7539-message.asc
new file mode 100644
index 000000000..d9627e9de
--- /dev/null
+++ b/tests/openpgp/issue7539-message.asc
@@ -0,0 +1,8 @@
+-----BEGIN PGP MESSAGE-----
+
+xA0DAQoWT96UsBf1XGsBywtiAAAAAAB0ZXN0CsJ1BAEWCgAdFiEE4nTJ+uve2SXH
+vtD4T96UsBf1XGsFAme5OzsACgkQT96UsBf1XGtKWAEAjmR2dUu8Jsvq+j3QArUQ
+J549CNsbbuHLLAhaE0C2zZMBAJD4hLT9KXxnpTINCAcgZfytWChkNP+qKqb4pV5N
+ItsH
+=OYzj
+-----END PGP MESSAGE-----
diff --git a/tests/openpgp/issue7539-signer.asc b/tests/openpgp/issue7539-signer.asc
new file mode 100644
index 000000000..170498e1e
--- /dev/null
+++ b/tests/openpgp/issue7539-signer.asc
@@ -0,0 +1,9 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xjMEZ7k39xYJKwYBBAHaRw8BAQdAOfCju+pxXLXR2WU7ItL1LdlJFfubUeXQPk33
+sqDgebXNCHRlc3Qga2V5wo8EEBYIADcCGQEFAme5N/cCGwMICwkIBwoNDAsFFQoJ
+CAsCFgIBJxYhBOJ0yfrr3tklx77Q+E/elLAX9VxrAAoJEE/elLAX9VxrJKcBAPzY
+8Ct8qZ2xbzMXMtHrnR+a2kYLVDA9U8xPtrzQOUcOAPoDW17PxLj0IyZBS7ewb2Zt
+bbZ7yHLYYKmrF2mAyBOiCA==
+=UNOn
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/openpgp/issue7539.scm b/tests/openpgp/issue7539.scm
new file mode 100644
index 000000000..c84c40feb
--- /dev/null
+++ b/tests/openpgp/issue7539.scm
@@ -0,0 +1,29 @@
+#!/usr/bin/env gpgscm
+
+;; Copyright (C) 2025 Daniel Kahn Gillmor <dkg at fifthhorseman.net>
+;;
+;; This file is part of GnuPG.
+;;
+;; GnuPG is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; GnuPG is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, see <https://www.gnu.org/licenses/>.
+
+(load (in-srcdir "tests" "openpgp" "defs.scm"))
+(setup-legacy-environment)
+
+(define keyfile (in-srcdir "tests" "openpgp" "issue7539-signer.asc"))
+(define msg (in-srcdir "tests" "openpgp" "issue7539-message.asc"))
+
+(info "Checking text Signature over binary Literal Data Packet")
+
+(call-check `(, at gpg --import ,keyfile))
+(call-check `(, at gpg --verify ,msg))
-- 
2.47.2




More information about the Gnupg-devel mailing list