Page MenuHome GnuPG

KDF-DO is not properly implemented
Testing, NormalPublic


According to the OpenPGP card specification (v3.4.1), the TLV is a constructed one and therefore, should return its own tag, length and the actual content; also the tag F9 indicates that it is a constructed TLV. See chapter 4.3.2. I think it is pretty clear, that an empty KDF-DO should return "F9 03 81 01 00". A valid KDF-DO should return "F9 xx 81 01 03 ...".

GnuPG seems to expect a primitive TLV, without the tag and length, e.g. in scd/app-openpgp.c "buffer[2]" (line 2500 in v2.4.5) is checked against 0x03, which doesn't match for a constructed TLV. Please note, that the length might not be one byte necessarily.

Also from the spec, it is not entirely clear to me what is the actual content of the DO and what the card should handle. I.e. should the card add the tag and length, or will that be part of GnuPG. Esp. because it reads " The content of the KDF-DO is not evaluated by any card command, the functionality is handled completely by the terminal application." In any case, the content of the KDF-DO is wrong in my opinion.



Event Timeline

werner added projects: gnupg, scd.

There are multiple problems described in your report. Let us handle one by one.

  • I confirmed that mistakes in GnuPG. There are data where constructed Data Objects are specified as primitive (data_objects in app-openpgp.c).
    • I'll fix this. So far, this doesn't cause any real problem though.
  • Possible problem of OpenPGP card implementation(s) for the OpenPGP card specification
    • Whether or not the tag+length should be returned for the constructed TLV, for GET DATA command.
  • GnuPG scdaemon behavior
    • It assumes that no tag+length for the data object F9

For the content of the DO (F9), card just stores the data content. It's GnuPG scdaemon (host side) to use the data to compute KEK (key encryption key) using KDF (key derivation function) when a user inputs his passphrase.

Given the situation where GnuPG works well with existing OpenPGP card implementations, what we should do here is, perhaps:

  • Allowing correct implementation which returns tag+length for F9 data objects for GET DATA command

Please keep in mind, that it is not only about GnuPG and the OpenPGP card, but also between GnuPG and other PGP applications. I'm not really sure what the recent commit is doing, if it only affect the reading or also the writing of the data. But IMHO GnuPG should stick to the standard also if writing the KDF DO data because eventually, it will be used for authentication with the card.

There are three cases:

  1. KDF DO stores the F9 tag+length (correct behavior, according to the standard)
  2. KDF DO stores just the content (old behavior)
  3. everything else, basically KDF DO stores garbage, ("The KDF-DO may be empty (set to NONE) or has an invalid value, in that case the standard password format (UTF-8) is valid.")

To be backwards compatible I'd propose the following: Store the KDF in the same format as it is already present on the card. For (3) use the new format, maybe ask the user first or make it an expert option.

I'll fix this. So far, this doesn't cause any real problem though.

Most probably, because of case (3) this will be overwritten with (2). FWIW, I stumbled across this because with my card, there is an option to enable KDF by default; it is a workaround for a card erratum for how the PINs are handled.

Allowing correct implementation which returns tag+length for F9 data objects for GET DATA command

And yes, this will probably fix my issue. I'll give it a try later. Just to confirm, all what is needed is the referenced commit? But again, GnuPG should adhere to the standard for interoperability with other PGP applications. But that is up to you to decide.

Please keep also in mind that the OpenPGP card specification has always and is still developed along with GnuPG . Thus if there are any uncertainties in the specification GnuPG's way of handling thing is the way to go. If there is a way to chnage things without risking any breakage we can of course fix that. In all other cases we need to continue wit the current way. For larger changes in the spec we can of course cleanup stuff - Achim is currently reworking on a revision.

I'm considering applying the following patch. With this change, scdaemon will works well with a card implementation which consider F9 (wrongly) as primitive data object, as well as correct card implementation.

diff --git a/scd/app-openpgp.c b/scd/app-openpgp.c
index 26ac91ea2..09223ce33 100644
--- a/scd/app-openpgp.c
+++ b/scd/app-openpgp.c
@@ -410,6 +410,10 @@ get_cached_data (app_t app, int tag,
   size_t len;
   struct cache_s *c;
   int exmode;
+  int do_constructed = 0;
+  if ((tag < 0x0100 && (tag & 0x20)) || (tag >= 0x0100 && (tag & 0x2000)))
+    do_constructed = 1;
   *result = NULL;
   *resultlen = 0;
@@ -452,6 +456,51 @@ get_cached_data (app_t app, int tag,
   err = iso7816_get_data (app_get_slot (app), exmode, tag, &p, &len);
   if (err)
     return err;
+  /* When Data Object is constructed, remove its tag and length before
+     storing it into the cache.  This handling also works well with
+     the card implementation which doesn't include its tag and length
+     for the DO value returned by GET DATA.  */
+  if (do_constructed)
+    {
+      if (len && tag < 0x0100 && p[0] == tag)
+        {
+          if (len >= 2 && p[1] < 0x80 && p[1] == len - 2)
+            {
+              len -= 2;
+              memmove (p, p+2, len);
+            }
+          else if (len >= 3 && p[1] == 0x81 && p[2] == len - 3)
+            {
+              len -= 3;
+              memmove (p, p+3, len);
+            }
+          else if (len >= 4 && p[1] == 0x82 && ((p[2] << 8) | p[3]) == len - 4)
+            {
+              len -= 4;
+              memmove (p, p+4, len);
+            }
+        }
+      else if (len >= 2 && tag >= 0x0100 && ((p[0] << 8) | p[1]) == tag)
+        {
+          if (len >= 3 && p[2] < 0x80 && p[2] == len - 2)
+            {
+              len -= 3;
+              memmove (p, p+3, len);
+            }
+          else if (len >= 4 && p[2] == 0x81 && p[3] == len - 3)
+            {
+              len -= 4;
+              memmove (p, p+4, len);
+            }
+          else if (len >= 5 && p[2] == 0x82 && ((p[3] << 8) | p[4]) == len - 4)
+            {
+              len -= 5;
+              memmove (p, p+5, len);
+            }
+        }
+    }
   if (len)
     *result = p;
   *resultlen = len;

FWIW, I've tested this patch and it works fine with both KDF as a constructed tag and as a primitive tag.

@mwalle Thank you for your testing.
Applied to master.
After testing, I'll also apply to 2.4 branch.

gniibe changed the task status from Open to Testing.Apr 22 2024, 8:07 AM

Applied to 2.4 branch.