[git] GnuPG - branch, master, updated. gnupg-2.1.11-148-g12af263

by Justus Winter cvs at cvs.gnupg.org
Thu Apr 21 14:45:20 CEST 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "The GNU Privacy Guard".

The branch, master has been updated
       via  12af2630cf4d1a39179179925fac8f2cce7504ff (commit)
       via  c6d1f2f08c68efe7e80887219064a8ce6365128f (commit)
       via  95303ee11df12f284e98d02dba993eda9e425383 (commit)
       via  342cc488890241b41e49f50886617115342721d6 (commit)
       via  0c35e09278514f1e3377a4b0a9b1f44dd39b1bf4 (commit)
      from  d81de224ecd542922dda649a492dd9550509d7bc (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 12af2630cf4d1a39179179925fac8f2cce7504ff
Author: Justus Winter <justus at g10code.com>
Date:   Fri Apr 8 19:21:12 2016 +0200

    common: Add support for the new extended private key format.
    
    * agent/findkey.c (write_extended_private_key): New function.
    (agent_write_private_key): Detect if an existing file is in extended
    format and update the key within if it is.
    (read_key_file): Handle the new format.
    * agent/keyformat.txt: Document the new format.
    * common/Makefile.am: Add the new files.
    * common/private-keys.c: New file.
    * common/private-keys.h: Likewise.
    * common/t-private-keys.c: Likewise.
    * common/util.h (alphap, alnump): New macros.
    * tests/migrations: Add test demonstrating that we can cope with the
    new format.
    
    --
    GnuPG 2.3+ will use a new format to store private keys that is both
    more flexible and easier to read and edit by human beings.  The new
    format stores name,value-pairs using the common mail and http header
    convention.
    
    This patch adds the parser and support code and prepares GnuPG 2.1 for
    the new format.
    
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/agent/findkey.c b/agent/findkey.c
index 3cf8d0c..a78709c 100644
--- a/agent/findkey.c
+++ b/agent/findkey.c
@@ -35,6 +35,7 @@
 #include "agent.h"
 #include "i18n.h"
 #include "../common/ssh-utils.h"
+#include "../common/private-keys.h"
 
 #ifndef O_BINARY
 #define O_BINARY 0
@@ -51,6 +52,75 @@ struct try_unprotect_arg_s
 };
 
 
+static gpg_error_t
+write_extended_private_key (char *fname, estream_t fp,
+                            const void *buf, size_t len)
+{
+  gpg_error_t err;
+  pkc_t pk = NULL;
+  gcry_sexp_t key = NULL;
+  int remove = 0;
+  int line;
+
+  err = pkc_parse (&pk, &line, fp);
+  if (err)
+    {
+      log_error ("error parsing '%s' line %d: %s\n",
+                 fname, line, gpg_strerror (err));
+      goto leave;
+    }
+
+  err = gcry_sexp_sscan (&key, NULL, buf, len);
+  if (err)
+    goto leave;
+
+  err = pkc_set_private_key (pk, key);
+  if (err)
+    goto leave;
+
+  err = es_fseek (fp, 0, SEEK_SET);
+  if (err)
+    goto leave;
+
+  err = pkc_write (pk, fp);
+  if (err)
+    {
+      log_error ("error writing '%s': %s\n", fname, gpg_strerror (err));
+      remove = 1;
+      goto leave;
+    }
+
+  if (ftruncate (es_fileno (fp), es_ftello (fp)))
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error truncating '%s': %s\n", fname, gpg_strerror (err));
+      remove = 1;
+      goto leave;
+    }
+
+  if (es_fclose (fp))
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error closing '%s': %s\n", fname, gpg_strerror (err));
+      remove = 1;
+      goto leave;
+    }
+  else
+    fp = NULL;
+
+  bump_key_eventcounter ();
+
+ leave:
+  if (fp)
+    es_fclose (fp);
+  if (remove)
+    gnupg_remove (fname);
+  xfree (fname);
+  gcry_sexp_release (key);
+  pkc_release (pk);
+  return err;
+}
+
 /* Write an S-expression formatted key to our key storage.  With FORCE
    passed as true an existing key with the given GRIP will get
    overwritten.  */
@@ -77,7 +147,7 @@ agent_write_private_key (const unsigned char *grip,
       return gpg_error (GPG_ERR_EEXIST);
     }
 
-  fp = es_fopen (fname, force? "wb,mode=-rw" : "wbx,mode=-rw");
+  fp = es_fopen (fname, force? "rb+,mode=-rw" : "wbx,mode=-rw");
   if (!fp)
     {
       gpg_error_t tmperr = gpg_error_from_syserror ();
@@ -86,6 +156,38 @@ agent_write_private_key (const unsigned char *grip,
       return tmperr;
     }
 
+  /* See if an existing key is in extended format.  */
+  if (force)
+    {
+      gpg_error_t rc;
+      char first;
+
+      if (es_fread (&first, 1, 1, fp) != 1)
+        {
+          rc = gpg_error_from_syserror ();
+          log_error ("error reading first byte from '%s': %s\n",
+                     fname, strerror (errno));
+          xfree (fname);
+          es_fclose (fp);
+          return rc;
+        }
+
+      rc = es_fseek (fp, 0, SEEK_SET);
+      if (rc)
+        {
+          log_error ("error seeking in '%s': %s\n", fname, strerror (errno));
+          xfree (fname);
+          es_fclose (fp);
+          return rc;
+        }
+
+      if (first != '(')
+        {
+          /* Key is in extended format.  */
+          return write_extended_private_key (fname, fp, buffer, length);
+        }
+    }
+
   if (es_fwrite (buffer, length, 1, fp) != 1)
     {
       gpg_error_t tmperr = gpg_error_from_syserror ();
@@ -95,6 +197,18 @@ agent_write_private_key (const unsigned char *grip,
       xfree (fname);
       return tmperr;
     }
+
+  /* When force is given, the file might have to be truncated.  */
+  if (force && ftruncate (es_fileno (fp), es_ftello (fp)))
+    {
+      gpg_error_t tmperr = gpg_error_from_syserror ();
+      log_error ("error truncating '%s': %s\n", fname, gpg_strerror (tmperr));
+      es_fclose (fp);
+      gnupg_remove (fname);
+      xfree (fname);
+      return tmperr;
+    }
+
   if (es_fclose (fp))
     {
       gpg_error_t tmperr = gpg_error_from_syserror ();
@@ -531,6 +645,7 @@ read_key_file (const unsigned char *grip, gcry_sexp_t *result)
   size_t buflen, erroff;
   gcry_sexp_t s_skey;
   char hexgrip[40+4+1];
+  char first;
 
   *result = NULL;
 
@@ -548,6 +663,50 @@ read_key_file (const unsigned char *grip, gcry_sexp_t *result)
       return rc;
     }
 
+  if (es_fread (&first, 1, 1, fp) != 1)
+    {
+      rc = gpg_error_from_syserror ();
+      log_error ("error reading first byte from '%s': %s\n",
+                 fname, strerror (errno));
+      xfree (fname);
+      es_fclose (fp);
+      return rc;
+    }
+
+  rc = es_fseek (fp, 0, SEEK_SET);
+  if (rc)
+    {
+      log_error ("error seeking in '%s': %s\n", fname, strerror (errno));
+      xfree (fname);
+      es_fclose (fp);
+      return rc;
+    }
+
+  if (first != '(')
+    {
+      /* Key is in extended format.  */
+      pkc_t pk;
+      int line;
+
+      rc = pkc_parse (&pk, &line, fp);
+      es_fclose (fp);
+
+      if (rc)
+        log_error ("error parsing '%s' line %d: %s\n",
+                   fname, line, gpg_strerror (rc));
+      else
+        {
+          rc = pkc_get_private_key (pk, result);
+          pkc_release (pk);
+          if (rc)
+            log_error ("error getting private key from '%s': %s\n",
+                       fname, gpg_strerror (rc));
+        }
+
+      xfree (fname);
+      return rc;
+    }
+
   if (fstat (es_fileno (fp), &st))
     {
       rc = gpg_error_from_syserror ();
diff --git a/agent/keyformat.txt b/agent/keyformat.txt
index 5e15ecf..c1a59ce 100644
--- a/agent/keyformat.txt
+++ b/agent/keyformat.txt
@@ -16,7 +16,71 @@ and should have permissions 700.
 The secret keys are stored in files with a name matching the
 hexadecimal representation of the keygrip[2] and suffixed with ".key".
 
-* Unprotected Private Key Format
+* Extended Private Key Format
+
+GnuPG 2.3+ will use a new format to store private keys that is both
+more flexible and easier to read and edit by human beings.  The new
+format stores name,value-pairs using the common mail and http header
+convention.  Example (here indented with two spaces):
+
+  Description: Key to sign all GnuPG released tarballs.
+    The key is actually stored on a smart card.
+  Use-for-ssh: yes
+  OpenSSH-cert: long base64 encoded string wrapped so that this
+    key file can be easily edited with a standard editor.
+  Key: (shadowed-private-key
+    (rsa
+    (n #00AA1AD2A55FD8C8FDE9E1941772D9CC903FA43B268CB1B5A1BAFDC900
+    2961D8AEA153424DC851EF13B83AC64FBE365C59DC1BD3E83017C90D4365B4
+    83E02859FC13DB5842A00E969480DB96CE6F7D1C03600392B8E08EF0C01FC7
+    19F9F9086B25AD39B4F1C2A2DF3E2BE317110CFFF21D4A11455508FE407997
+    601260816C8422297C0637BB291C3A079B9CB38A92CE9E551F80AA0EBF4F0E
+    72C3F250461E4D31F23A7087857FC8438324A013634563D34EFDDCBF2EA80D
+    F9662C9CCD4BEF2522D8BDFED24CEF78DC6B309317407EAC576D889F88ADA0
+    8C4FFB480981FB68C5C6CA27503381D41018E6CDC52AAAE46B166BDC10637A
+    E186A02BA2497FDC5D1221#)
+    (e #00010001#)
+    (shadowed t1-v1
+     (#D2760001240102000005000011730000# OPENPGP.1)
+    )))
+
+GnuPG 2.2 is able to read and update keys using the new format, but
+will not create new files using the new format.  Furthermore, it only
+makes use of the value stored under the name 'Key:'.
+
+Keys in the extended format can be recognized by looking at the first
+byte of the file.  If it starts with a '(' it is a naked S-expression,
+otherwise it is a key in extended format.
+
+** Names
+
+A name must start with a letter and end with a colon.  Valid
+characters are all ASCII letters, numbers and the hyphen.  Comparison
+of names is done case insensitively.  Names may be used several times
+to represent an array of values.
+
+The name "Key:" is special in that it may occur only once and the
+associated value holds the actual S-expression with the cryptographic
+key.  The S-expression is formatted using the 'Advanced Format'
+(GCRYSEXP_FMT_ADVANCED) that avoids non-printable characters so that
+the file can be easily inspected and edited.  See section 'Private Key
+Format' below for details.
+
+** Values
+
+Values are UTF-8 encoded strings.  Values can be wrapped at any point,
+and continued in the next line indicated by leading whitespace.  A
+continuation line with one leading space does not introduce a blank so
+that the lines can be effectively concatenated.  A blank line as part
+of a continuation line encodes a newline.
+
+** Comments
+
+Lines containing only whitespace, and lines starting with whitespace
+followed by '#' are considered to be comments and are ignored.
+
+* Private Key Format
+** Unprotected Private Key Format
 
 The content of the file is an S-Expression like the ones used with
 Libgcrypt.  Here is an example of an unprotected file:
@@ -42,7 +106,7 @@ optional but required for some operations to calculate the fingerprint
 of the key.  This timestamp should be a string with the number of
 seconds since Epoch or an ISO time string (yyyymmddThhmmss).
 
-* Protected Private Key Format
+** Protected Private Key Format
 
 A protected key is like this:
 
@@ -67,7 +131,7 @@ optional; the isotimestamp is 15 bytes long (e.g. "19610711T172000").
 
 The currently defined protection modes are:
 
-** openpgp-s2k3-sha1-aes-cbc
+*** openpgp-s2k3-sha1-aes-cbc
 
   This describes an algorithm using using AES in CBC mode for
   encryption, SHA-1 for integrity protection and the String to Key
@@ -116,7 +180,7 @@ The currently defined protection modes are:
   the stored one - If they don't match the integrity of the key is not
   given.
 
-** openpgp-s2k3-ocb-aes
+*** openpgp-s2k3-ocb-aes
 
   This describes an algorithm using using AES-128 in OCB mode, a nonce
   of 96 bit, a taglen of 128 bit, and the String to Key algorithm 3
@@ -154,7 +218,7 @@ The currently defined protection modes are:
    (protected-at "18950523T000000")
   )
 
-** openpgp-native
+*** openpgp-native
 
   This is a wrapper around the OpenPGP Private Key Transport format
   which resembles the standard OpenPGP format and allows the use of an
@@ -191,7 +255,7 @@ The currently defined protection modes are:
    (uri http://foo.bar x-foo:whatever_you_want)
    (comment whatever))
 
-* Shadowed Private Key Format
+** Shadowed Private Key Format
 
 To keep track of keys stored on IC cards we use a third format for
 private kyes which are called shadow keys as they are only a reference
@@ -219,7 +283,7 @@ readers don't allow passing a variable length PIN.
 
 More items may be added to the list.
 
-* OpenPGP Private Key Transfer Format
+** OpenPGP Private Key Transfer Format
 
 This format is used to transfer keys between gpg and gpg-agent.
 
@@ -251,7 +315,7 @@ This format is used to transfer keys between gpg and gpg-agent.
  * S2KSALT is the 8 byte salt
  * S2KCOUNT is the count value from RFC-4880.
 
-* Persistent Passphrase Format
+** Persistent Passphrase Format
 
 Note: That this has not yet been implemented.
 
diff --git a/common/Makefile.am b/common/Makefile.am
index de6a4a8..4a35f64 100644
--- a/common/Makefile.am
+++ b/common/Makefile.am
@@ -89,7 +89,8 @@ common_sources = \
 	strlist.c strlist.h \
 	call-gpg.c call-gpg.h \
 	exectool.c exectool.h \
-	server-help.c server-help.h
+	server-help.c server-help.h \
+	private-keys.c private-keys.h
 
 if HAVE_W32_SYSTEM
 common_sources += w32-reg.c w32-afunix.c w32-afunix.h
@@ -154,7 +155,8 @@ endif
 module_tests = t-stringhelp t-timestuff \
                t-convert t-percent t-gettime t-sysutils t-sexputil \
 	       t-session-env t-openpgp-oid t-ssh-utils \
-	       t-mapstrings t-zb32 t-mbox-util t-iobuf t-strlist
+	       t-mapstrings t-zb32 t-mbox-util t-iobuf t-strlist \
+	       t-private-keys
 if !HAVE_W32CE_SYSTEM
 module_tests += t-exechelp
 endif
@@ -203,6 +205,7 @@ t_zb32_LDADD = $(t_common_ldadd)
 t_mbox_util_LDADD = $(t_common_ldadd)
 t_iobuf_LDADD = $(t_common_ldadd)
 t_strlist_LDADD = $(t_common_ldadd)
+t_private_keys_LDADD = $(t_common_ldadd)
 
 # System specific test
 if HAVE_W32_SYSTEM
diff --git a/common/private-keys.c b/common/private-keys.c
new file mode 100644
index 0000000..d77ce16
--- /dev/null
+++ b/common/private-keys.c
@@ -0,0 +1,740 @@
+/* private-keys.c - Parser and writer for the extended private key format.
+ *	Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of either
+ *
+ *   - the GNU Lesser General Public License as published by the Free
+ *     Software Foundation; either version 3 of the License, or (at
+ *     your option) any later version.
+ *
+ * or
+ *
+ *   - the GNU General Public License as published by the Free
+ *     Software Foundation; either version 2 of the License, or (at
+ *     your option) any later version.
+ *
+ * or both in parallel, as here.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <assert.h>
+#include <gcrypt.h>
+#include <gpg-error.h>
+#include <string.h>
+
+#include "private-keys.h"
+#include "mischelp.h"
+#include "strlist.h"
+#include "util.h"
+
+struct private_key_container
+{
+  struct private_key_entry *first;
+  struct private_key_entry *last;
+};
+
+
+struct private_key_entry
+{
+  struct private_key_entry *prev;
+  struct private_key_entry *next;
+
+  /* The name.  Comments and blank lines have NAME set to NULL.  */
+  char *name;
+
+  /* The value as stored in the file.  We store it when when we parse
+     a file so that we can reproduce it.  */
+  strlist_t raw_value;
+
+  /* The decoded value.  */
+  char *value;
+};
+
+

+
+/* Allocation and deallocation.  */
+
+/* Allocate a private key container structure.  */
+pkc_t
+pkc_new (void)
+{
+  return xtrycalloc (1, sizeof (struct private_key_container));
+}
+
+
+static void
+pke_release (pke_t entry)
+{
+  if (entry == NULL)
+    return;
+
+  xfree (entry->name);
+  if (entry->value)
+    wipememory (entry->value, strlen (entry->value));
+  xfree (entry->value);
+  free_strlist_wipe (entry->raw_value);
+  xfree (entry);
+}
+
+
+/* Release a private key container structure.  */
+void
+pkc_release (pkc_t pk)
+{
+  pke_t e, next;
+
+  if (pk == NULL)
+    return;
+
+  for (e = pk->first; e; e = next)
+    {
+      next = e->next;
+      pke_release (e);
+    }
+
+  xfree (pk);
+}
+
+

+
+/* Dealing with names and values.  */
+
+/* Check whether the given name is valid.  Valid names start with a
+   letter, end with a colon, and contain only alphanumeric characters
+   and the hyphen.  */
+static int
+valid_name (const char *name)
+{
+  size_t i, len = strlen (name);
+
+  if (! alphap (name) || len == 0 || name[len - 1] != ':')
+    return 0;
+
+  for (i = 1; i < len - 1; i++)
+    if (! alnump (&name[i]) && name[i] != '-')
+      return 0;
+
+  return 1;
+}
+
+
+/* Makes sure that ENTRY has a RAW_VALUE.  */
+static gpg_error_t
+assert_raw_value (pke_t entry)
+{
+  gpg_error_t err = 0;
+  size_t len, offset;
+#define LINELEN	70
+  char buf[LINELEN+3];
+
+  if (entry->raw_value)
+    return 0;
+
+  len = strlen (entry->value);
+  offset = 0;
+  while (len)
+    {
+      size_t amount, linelen = LINELEN;
+
+      /* On the first line we need to subtract space for the name.  */
+      if (entry->raw_value == NULL && strlen (entry->name) < linelen)
+	linelen -= strlen (entry->name);
+
+      /* See if the rest of the value fits in this line.  */
+      if (len <= linelen)
+	amount = len;
+      else
+	{
+	  size_t i;
+
+	  /* Find a suitable space to break on.  */
+	  for (i = linelen - 1; linelen - i < 30 && linelen - i > offset; i--)
+	    if (ascii_isspace (entry->value[i]))
+	      break;
+
+	  if (ascii_isspace (entry->value[i]))
+	    {
+	      /* Found one.  */
+	      amount = i;
+	    }
+	  else
+	    {
+	      /* Just induce a hard break.  */
+	      amount = linelen;
+	    }
+	}
+
+      snprintf (buf, sizeof buf, " %.*s\n", (int) amount,
+		&entry->value[offset]);
+      if (append_to_strlist_try (&entry->raw_value, buf) == NULL)
+	{
+	  err = gpg_error_from_syserror ();
+	  goto leave;
+	}
+
+      offset += amount;
+      len -= amount;
+    }
+
+ leave:
+  if (err)
+    {
+      free_strlist_wipe (entry->raw_value);
+      entry->raw_value = NULL;
+    }
+
+  return err;
+#undef LINELEN
+}
+
+
+/* Computes the length of the value encoded as continuation.  If
+   *SWALLOW_WS is set, all whitespace at the beginning of S is
+   swallowed.  If START is given, a pointer to the beginning of the
+   value is stored there.  */
+static size_t
+continuation_length (const char *s, int *swallow_ws, const char **start)
+{
+  size_t len;
+
+  if (*swallow_ws)
+    {
+      /* The previous line was a blank line and we inserted a newline.
+	 Swallow all whitespace at the beginning of this line.  */
+      while (ascii_isspace (*s))
+	s++;
+    }
+  else
+    {
+      /* Iff a continuation starts with more than one space, it
+	 encodes a space.  */
+      if (ascii_isspace (*s))
+	s++;
+    }
+
+  /* Strip whitespace at the end.  */
+  len = strlen (s);
+  while (len > 0 && ascii_isspace (s[len-1]))
+    len--;
+
+  if (len == 0)
+    {
+      /* Blank lines encode newlines.  */
+      len = 1;
+      s = "\n";
+      *swallow_ws = 1;
+    }
+  else
+    *swallow_ws = 0;
+
+  if (start)
+    *start = s;
+
+  return len;
+}
+
+
+/* Makes sure that ENTRY has a VALUE.  */
+static gpg_error_t
+assert_value (pke_t entry)
+{
+  size_t len;
+  int swallow_ws;
+  strlist_t s;
+  char *p;
+
+  if (entry->value)
+    return 0;
+
+  len = 0;
+  swallow_ws = 0;
+  for (s = entry->raw_value; s; s = s->next)
+    len += continuation_length (s->d, &swallow_ws, NULL);
+
+  /* Add one for the terminating zero.  */
+  len += 1;
+
+  entry->value = p = xtrymalloc (len);
+  if (entry->value == NULL)
+    return gpg_error_from_syserror ();
+
+  swallow_ws = 0;
+  for (s = entry->raw_value; s; s = s->next)
+    {
+      const char *start;
+      size_t l = continuation_length (s->d, &swallow_ws, &start);
+
+      memcpy (p, start, l);
+      p += l;
+    }
+
+  *p++ = 0;
+  assert (p - entry->value == len);
+
+  return 0;
+}
+
+
+/* Get the name.  */
+char *
+pke_name (pke_t pke)
+{
+  return pke->name;
+}
+
+
+/* Get the value.  */
+char *
+pke_value (pke_t pke)
+{
+  if (assert_value (pke))
+    return NULL;
+  return pke->value;
+}
+
+

+
+/* Adding and modifying values.  */
+
+/* Add (NAME, VALUE, RAW_VALUE) to PK.  NAME may be NULL for comments
+   and blank lines.  At least one of VALUE and RAW_VALUE must be
+   given.  If PRESERVE_ORDER is not given, entries with the same name
+   are grouped.  NAME, VALUE and RAW_VALUE is consumed.  */
+static gpg_error_t
+_pkc_add (pkc_t pk, char *name, char *value, strlist_t raw_value,
+	  int preserve_order)
+{
+  gpg_error_t err = 0;
+  pke_t e;
+
+  assert (value || raw_value);
+
+  if (name && ! valid_name (name))
+    {
+      err = gpg_error (GPG_ERR_INV_NAME);
+      goto leave;
+    }
+
+  if (name && strcasecmp (name, "Key:") == 0 && pkc_lookup (pk, "Key:"))
+    {
+      err = gpg_error (GPG_ERR_INV_NAME);
+      goto leave;
+    }
+
+  e = xtrycalloc (1, sizeof *e);
+  if (e == NULL)
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+
+  e->name = name;
+  e->value = value;
+  e->raw_value = raw_value;
+
+  if (pk->first)
+    {
+      pke_t last;
+
+      if (preserve_order)
+	last = pk->last;
+      else
+	{
+	  /* See if there is already an entry with NAME.  */
+	  last = pkc_lookup (pk, name);
+
+	  /* If so, find the last in that block.  */
+	  if (last)
+	    while (last->next)
+	      {
+		pke_t next = last->next;
+
+		if (next->name && strcasecmp (next->name, name) == 0)
+		  last = next;
+		else
+		  break;
+	      }
+	  /* Otherwise, just find the last entry.  */
+	  else
+	    last = pk->last;
+	}
+
+      if (last->next)
+	{
+	  e->prev = last;
+	  e->next = last->next;
+	  last->next = e;
+	  e->next->prev = e;
+	}
+      else
+	{
+	  e->prev = last;
+	  last->next = e;
+	  pk->last = e;
+	}
+    }
+  else
+    pk->first = pk->last = e;
+
+ leave:
+  if (err)
+    {
+      xfree (name);
+      if (value)
+	wipememory (value, strlen (value));
+      xfree (value);
+      free_strlist_wipe (raw_value);
+    }
+
+  return err;
+}
+
+
+/* Add (NAME, VALUE) to PK.  If an entry with NAME already exists, it
+   is not updated but the new entry is appended.  */
+gpg_error_t
+pkc_add (pkc_t pk, const char *name, const char *value)
+{
+  char *k, *v;
+
+  k = xtrystrdup (name);
+  if (k == NULL)
+    return gpg_error_from_syserror ();
+
+  v = xtrystrdup (value);
+  if (v == NULL)
+    {
+      xfree (k);
+      return gpg_error_from_syserror ();
+    }
+
+  return _pkc_add (pk, k, v, NULL, 0);
+}
+
+
+/* Add (NAME, VALUE) to PK.  If an entry with NAME already exists, it
+   is updated with VALUE.  If multiple entries with NAME exist, the
+   first entry is updated.  */
+gpg_error_t
+pkc_set (pkc_t pk, const char *name, const char *value)
+{
+  pke_t e;
+
+  if (! valid_name (name))
+    return GPG_ERR_INV_NAME;
+
+  e = pkc_lookup (pk, name);
+  if (e)
+    {
+      char *v;
+
+      v = xtrystrdup (value);
+      if (v == NULL)
+	return gpg_error_from_syserror ();
+
+      free_strlist_wipe (e->raw_value);
+      e->raw_value = NULL;
+      if (e->value)
+	wipememory (e->value, strlen (e->value));
+      xfree (e->value);
+      e->value = v;
+
+      return 0;
+    }
+  else
+    return pkc_add (pk, name, value);
+}
+
+
+/* Delete the given entry from PK.  */
+void
+pkc_delete (pkc_t pk, pke_t entry)
+{
+  if (entry->prev)
+    entry->prev->next = entry->next;
+  else
+    pk->first = entry->next;
+
+  if (entry->next)
+    entry->next->prev = entry->prev;
+  else
+    pk->last = entry->prev;
+
+  pke_release (entry);
+}
+
+

+
+/* Lookup and iteration.  */
+
+/* Get the first non-comment entry.  */
+pke_t
+pkc_first (pkc_t pk)
+{
+  pke_t entry;
+  for (entry = pk->first; entry; entry = entry->next)
+    if (entry->name)
+      return entry;
+  return NULL;
+}
+
+
+/* Get the first entry with the given name.  */
+pke_t
+pkc_lookup (pkc_t pk, const char *name)
+{
+  pke_t entry;
+  for (entry = pk->first; entry; entry = entry->next)
+    if (entry->name && strcasecmp (entry->name, name) == 0)
+      return entry;
+  return NULL;
+}
+
+
+/* Get the next non-comment entry.  */
+pke_t
+pke_next (pke_t entry)
+{
+  for (entry = entry->next; entry; entry = entry->next)
+    if (entry->name)
+      return entry;
+  return NULL;
+}
+
+
+/* Get the next entry with the given name.  */
+pke_t
+pke_next_value (pke_t entry, const char *name)
+{
+  for (entry = entry->next; entry; entry = entry->next)
+    if (entry->name && strcasecmp (entry->name, name) == 0)
+      return entry;
+  return NULL;
+}
+
+

+
+/* Private key handling.  */
+
+/* Get the private key.  */
+gpg_error_t
+pkc_get_private_key (pkc_t pk, gcry_sexp_t *retsexp)
+{
+  gpg_error_t err;
+  pke_t e;
+
+  e = pkc_lookup (pk, "Key:");
+  if (e == NULL)
+    return gpg_error (GPG_ERR_MISSING_KEY);
+
+  err = assert_value (e);
+  if (err)
+    return err;
+
+  return gcry_sexp_sscan (retsexp, NULL, e->value, strlen (e->value));
+}
+
+
+/* Set the private key.  */
+gpg_error_t
+pkc_set_private_key (pkc_t pk, gcry_sexp_t sexp)
+{
+  gpg_error_t err;
+  char *raw, *clean, *p;
+  size_t len, i;
+
+  len = gcry_sexp_sprint (sexp, GCRYSEXP_FMT_ADVANCED, NULL, 0);
+
+  raw = xtrymalloc (len);
+  if (raw == NULL)
+    return gpg_error_from_syserror ();
+
+  clean = xtrymalloc (len);
+  if (clean == NULL)
+    {
+      xfree (raw);
+      return gpg_error_from_syserror ();
+    }
+
+  gcry_sexp_sprint (sexp, GCRYSEXP_FMT_ADVANCED, raw, len);
+
+  /* Strip any whitespace at the end.  */
+  i = strlen (raw) - 1;
+  while (i && ascii_isspace (raw[i]))
+    {
+      raw[i] = 0;
+      i--;
+    }
+
+  /* Replace any newlines with spaces, remove superfluous whitespace.  */
+  len = strlen (raw);
+  for (p = clean, i = 0; i < len; i++)
+    {
+      char c = raw[i];
+
+      /* Collapse contiguous and superfluous spaces.  */
+      if (ascii_isspace (c) && i > 0
+	  && (ascii_isspace (raw[i-1]) || raw[i-1] == '(' || raw[i-1] == ')'))
+	continue;
+
+      if (c == '\n')
+	c = ' ';
+
+      *p++ = c;
+    }
+  *p = 0;
+
+  err = pkc_set (pk, "Key:", clean);
+  xfree (raw);
+  xfree (clean);
+  return err;
+}
+
+

+
+/* Parsing and serialization.  */
+
+/* Parse STREAM and return a newly allocated private key container
+   structure in RESULT.  If ERRLINEP is given, the line number the
+   parser was last considering is stored there.  */
+gpg_error_t
+pkc_parse (pkc_t *result, int *errlinep, estream_t stream)
+{
+  gpg_error_t err = 0;
+  gpgrt_ssize_t len;
+  char *buf = NULL;
+  size_t buf_len = 0;
+  char *name = NULL;
+  strlist_t raw_value = NULL;
+
+
+  *result = pkc_new ();
+  if (*result == NULL)
+    return gpg_error_from_syserror ();
+
+  if (errlinep)
+    *errlinep = 0;
+  while ((len = es_read_line (stream, &buf, &buf_len, NULL)))
+    {
+      char *p;
+      if (errlinep)
+	*errlinep += 1;
+
+      /* Skip any whitespace.  */
+      for (p = buf; *p && ascii_isspace (*p); p++)
+	/* Do nothing.  */;
+
+      if (name && (spacep (buf) || *p == 0))
+	{
+	  /* A continuation.  */
+	  if (append_to_strlist_try (&raw_value, buf) == NULL)
+	    {
+	      err = gpg_error_from_syserror ();
+	      goto leave;
+	    }
+	  continue;
+	}
+
+      /* No continuation.  Add the current entry if any.  */
+      if (raw_value)
+	{
+	  err = _pkc_add (*result, name, NULL, raw_value, 1);
+	  if (err)
+	    goto leave;
+	}
+
+      /* And prepare for the next one.  */
+      name = NULL;
+      raw_value = NULL;
+
+      if (*p != 0 && *p != '#')
+	{
+	  char *colon, *value, tmp;
+
+	  colon = strchr (buf, ':');
+	  if (colon == NULL)
+	    {
+	      err = gpg_error (GPG_ERR_INV_VALUE);
+	      goto leave;
+	    }
+
+	  value = colon + 1;
+	  tmp = *value;
+	  *value = 0;
+	  name = xstrdup (p);
+	  *value = tmp;
+
+	  if (name == NULL)
+	    {
+	      err = gpg_error_from_syserror ();
+	      goto leave;
+	    }
+
+	  if (append_to_strlist (&raw_value, value) == NULL)
+	    {
+	      err = gpg_error_from_syserror ();
+	      goto leave;
+	    }
+	  continue;
+	}
+
+      if (append_to_strlist (&raw_value, buf) == NULL)
+	{
+	  err = gpg_error_from_syserror ();
+	  goto leave;
+	}
+    }
+
+  /* Add the final entry.  */
+  if (raw_value)
+    err = _pkc_add (*result, name, NULL, raw_value, 1);
+
+ leave:
+  gpgrt_free (buf);
+  if (err)
+    {
+      pkc_release (*result);
+      *result = NULL;
+    }
+
+  return err;
+}
+
+
+/* Write a representation of PK to STREAM.  */
+gpg_error_t
+pkc_write (pkc_t pk, estream_t stream)
+{
+  gpg_error_t err;
+  pke_t entry;
+  strlist_t s;
+
+  for (entry = pk->first; entry; entry = entry->next)
+    {
+      if (entry->name)
+	es_fputs (entry->name, stream);
+
+      err = assert_raw_value (entry);
+      if (err)
+	return err;
+
+      for (s = entry->raw_value; s; s = s->next)
+	es_fputs (s->d, stream);
+
+      if (es_ferror (stream))
+	return gpg_error_from_syserror ();
+    }
+
+  return 0;
+}
diff --git a/common/private-keys.h b/common/private-keys.h
new file mode 100644
index 0000000..d21e94f
--- /dev/null
+++ b/common/private-keys.h
@@ -0,0 +1,109 @@
+/* private-keys.h - Parser and writer for the extended private key format.
+ *	Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of either
+ *
+ *   - the GNU Lesser General Public License as published by the Free
+ *     Software Foundation; either version 3 of the License, or (at
+ *     your option) any later version.
+ *
+ * or
+ *
+ *   - the GNU General Public License as published by the Free
+ *     Software Foundation; either version 2 of the License, or (at
+ *     your option) any later version.
+ *
+ * or both in parallel, as here.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GNUPG_COMMON_PRIVATE_KEYS_H
+#define GNUPG_COMMON_PRIVATE_KEYS_H
+
+struct private_key_container;
+typedef struct private_key_container *pkc_t;
+
+struct private_key_entry;
+typedef struct private_key_entry *pke_t;
+
+

+
+/* Memory management, and dealing with entries.  */
+
+/* Allocate a private key container structure.  */
+pkc_t pkc_new (void);
+
+/* Release a private key container structure.  */
+void pkc_release (pkc_t pk);
+
+/* Get the name.  */
+char *pke_name (pke_t pke);
+
+/* Get the value.  */
+char *pke_value (pke_t pke);
+
+

+
+/* Lookup and iteration.  */
+
+/* Get the first non-comment entry.  */
+pke_t pkc_first (pkc_t pk);
+
+/* Get the first entry with the given name.  */
+pke_t pkc_lookup (pkc_t pk, const char *name);
+
+/* Get the next non-comment entry.  */
+pke_t pke_next (pke_t entry);
+
+/* Get the next entry with the given name.  */
+pke_t pke_next_value (pke_t entry, const char *name);
+
+

+
+/* Adding and modifying values.  */
+
+/* Add (NAME, VALUE) to PK.  If an entry with NAME already exists, it
+   is not updated but the new entry is appended.  */
+gpg_error_t pkc_add (pkc_t pk, const char *name, const char *value);
+
+/* Add (NAME, VALUE) to PK.  If an entry with NAME already exists, it
+   is updated with VALUE.  If multiple entries with NAME exist, the
+   first entry is updated.  */
+gpg_error_t pkc_set (pkc_t pk, const char *name, const char *value);
+
+/* Delete the given entry from PK.  */
+void pkc_delete (pkc_t pk, pke_t pke);
+
+

+
+/* Private key handling.  */
+
+/* Get the private key.  */
+gpg_error_t pkc_get_private_key (pkc_t pk, gcry_sexp_t *retsexp);
+
+/* Set the private key.  */
+gpg_error_t pkc_set_private_key (pkc_t pk, gcry_sexp_t sexp);
+
+

+
+/* Parsing and serialization.  */
+
+/* Parse STREAM and return a newly allocated private key container
+   structure in RESULT.  If ERRLINEP is given, the line number the
+   parser was last considering is stored there.  */
+gpg_error_t pkc_parse (pkc_t *result, int *errlinep, estream_t stream);
+
+/* Write a representation of PK to STREAM.  */
+gpg_error_t pkc_write (pkc_t pk, estream_t stream);
+
+#endif /* GNUPG_COMMON_PRIVATE_KEYS_H */
diff --git a/common/t-private-keys.c b/common/t-private-keys.c
new file mode 100644
index 0000000..06415a1
--- /dev/null
+++ b/common/t-private-keys.c
@@ -0,0 +1,543 @@
+/* t-private-keys.c - Module test for private-keys.c
+ *	Copyright (C) 2016 g10 Code GmbH
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <assert.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "util.h"
+#include "private-keys.h"
+
+static int verbose;
+
+void
+test_getting_values (pkc_t pk)
+{
+  pke_t e;
+
+  e = pkc_lookup (pk, "Comment:");
+  assert (e);
+
+  /* Names are case-insensitive.  */
+  e = pkc_lookup (pk, "comment:");
+  assert (e);
+  e = pkc_lookup (pk, "COMMENT:");
+  assert (e);
+
+  e = pkc_lookup (pk, "SomeOtherName:");
+  assert (e);
+}
+
+
+void
+test_key_extraction (pkc_t pk)
+{
+  gpg_error_t err;
+  gcry_sexp_t key;
+
+  err = pkc_get_private_key (pk, &key);
+  assert (err == 0);
+  assert (key);
+
+  if (verbose)
+    gcry_sexp_dump (key);
+
+  gcry_sexp_release (key);
+}
+
+
+void
+test_iteration (pkc_t pk)
+{
+  int i;
+  pke_t e;
+
+  i = 0;
+  for (e = pkc_first (pk); e; e = pke_next (e))
+    i++;
+  assert (i == 4);
+
+  i = 0;
+  for (e = pkc_lookup (pk, "Comment:");
+       e;
+       e = pke_next_value (e, "Comment:"))
+    i++;
+  assert (i == 3);
+}
+
+
+void
+test_whitespace (pkc_t pk)
+{
+  pke_t e;
+
+  e = pkc_lookup (pk, "One:");
+  assert (e);
+  assert (strcmp (pke_value (e), "WithoutWhitespace") == 0);
+
+  e = pkc_lookup (pk, "Two:");
+  assert (e);
+  assert (strcmp (pke_value (e), "With Whitespace") == 0);
+
+  e = pkc_lookup (pk, "Three:");
+  assert (e);
+  assert (strcmp (pke_value (e),
+                  "Blank lines in continuations encode newlines.\n"
+                  "Next paragraph.") == 0);
+}
+
+
+struct
+{
+  char *value;
+  void (*test_func) (pkc_t);
+} tests[] =
+  {
+    {
+      "# This is a comment followed by an empty line\n"
+      "\n",
+      NULL,
+    },
+    {
+      "# This is a comment followed by two empty lines, Windows style\r\n"
+      "\r\n"
+      "\r\n",
+      NULL,
+    },
+    {
+      "# Some name,value pairs\n"
+      "Comment: Some comment.\n"
+      "SomeOtherName: Some value.\n",
+      test_getting_values,
+    },
+    {
+      "  # Whitespace is preserved as much as possible\r\n"
+      "Comment:Some comment.\n"
+      "SomeOtherName: Some value.   \n",
+      test_getting_values,
+    },
+    {
+      "# Values may be continued in the next line as indicated by leading\n"
+      "# space\n"
+      "Comment: Some rather long\n"
+      "  comment that is continued in the next line.\n"
+      "\n"
+      "  Blank lines with or without whitespace are allowed within\n"
+      "  continuations to allow paragraphs.\n"
+      "SomeOtherName: Some value.\n",
+      test_getting_values,
+    },
+    {
+      "# Names may be given multiple times forming an array of values\n"
+      "Comment: Some comment, element 0.\n"
+      "Comment: Some comment, element 1.\n"
+      "Comment: Some comment, element 2.\n"
+      "SomeOtherName: Some value.\n",
+      test_iteration,
+    },
+    {
+      "# One whitespace at the beginning of a continuation is swallowed.\n"
+      "One: Without\n"
+      " Whitespace\n"
+      "Two: With\n"
+      "  Whitespace\n"
+      "Three: Blank lines in continuations encode newlines.\n"
+      "\n"
+      "  Next paragraph.\n",
+      test_whitespace,
+    },
+    {
+      "Description: Key to sign all GnuPG released tarballs.\n"
+      "  The key is actually stored on a smart card.\n"
+      "Use-for-ssh: yes\n"
+      "OpenSSH-cert: long base64 encoded string wrapped so that this\n"
+      "  key file can be easily edited with a standard editor.\n"
+      "Key: (shadowed-private-key\n"
+      "  (rsa\n"
+      "  (n #00AA1AD2A55FD8C8FDE9E1941772D9CC903FA43B268CB1B5A1BAFDC900\n"
+      "  2961D8AEA153424DC851EF13B83AC64FBE365C59DC1BD3E83017C90D4365B4\n"
+      "  83E02859FC13DB5842A00E969480DB96CE6F7D1C03600392B8E08EF0C01FC7\n"
+      "  19F9F9086B25AD39B4F1C2A2DF3E2BE317110CFFF21D4A11455508FE407997\n"
+      "  601260816C8422297C0637BB291C3A079B9CB38A92CE9E551F80AA0EBF4F0E\n"
+      "  72C3F250461E4D31F23A7087857FC8438324A013634563D34EFDDCBF2EA80D\n"
+      "  F9662C9CCD4BEF2522D8BDFED24CEF78DC6B309317407EAC576D889F88ADA0\n"
+      "  8C4FFB480981FB68C5C6CA27503381D41018E6CDC52AAAE46B166BDC10637A\n"
+      "  E186A02BA2497FDC5D1221#)\n"
+      "  (e #00010001#)\n"
+      "  (shadowed t1-v1\n"
+      "   (#D2760001240102000005000011730000# OPENPGP.1)\n"
+      "    )))\n",
+      test_key_extraction,
+    },
+  };
+
+
+static char *
+pkc_to_string (pkc_t pk)
+{
+  gpg_error_t err;
+  char *buf;
+  size_t len;
+  estream_t sink;
+
+  sink = es_fopenmem (0, "rw");
+  assert (sink);
+
+  err = pkc_write (pk, sink);
+  assert (err == 0);
+
+  len = es_ftell (sink);
+  buf = xmalloc (len+1);
+  assert (buf);
+
+  es_fseek (sink, 0, SEEK_SET);
+  es_read (sink, buf, len, NULL);
+  buf[len] = 0;
+
+  es_fclose (sink);
+  return buf;
+}
+
+
+void dummy_free (void *p) { (void) p; }
+void *dummy_realloc (void *p, size_t s) { (void) s; return p; }
+
+void
+run_tests (void)
+{
+  gpg_error_t err;
+  pkc_t pk;
+
+  int i;
+  for (i = 0; i < DIM (tests); i++)
+    {
+      estream_t source;
+      char *buf;
+      size_t len;
+
+      len = strlen (tests[i].value);
+      source = es_mopen (tests[i].value, len, len,
+			 0, dummy_realloc, dummy_free, "r");
+      assert (source);
+
+      err = pkc_parse (&pk, NULL, source);
+      assert (err == 0);
+      assert (pk);
+
+      if (verbose)
+	{
+	  err = pkc_write (pk, es_stderr);
+	  assert (err == 0);
+	}
+
+      buf = pkc_to_string (pk);
+      assert (memcmp (tests[i].value, buf, len) == 0);
+
+      es_fclose (source);
+      xfree (buf);
+
+      if (tests[i].test_func)
+	tests[i].test_func (pk);
+
+      pkc_release (pk);
+    }
+}
+
+
+void
+run_modification_tests (void)
+{
+  gpg_error_t err;
+  pkc_t pk;
+  pke_t e;
+  gcry_sexp_t key;
+  char *buf;
+
+  pk = pkc_new ();
+  assert (pk);
+
+  pkc_set (pk, "Foo:", "Bar");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Bar\n") == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Foo:", "Baz");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\n") == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Bar:", "Bazzel");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\nBar: Bazzel\n") == 0);
+  xfree (buf);
+
+  pkc_add (pk, "Foo:", "Bar");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\nFoo: Bar\nBar: Bazzel\n") == 0);
+  xfree (buf);
+
+  pkc_add (pk, "DontExistYet:", "Bar");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\nFoo: Bar\nBar: Bazzel\nDontExistYet: Bar\n")
+	  == 0);
+  xfree (buf);
+
+  pkc_delete (pk, pkc_lookup (pk, "DontExistYet:"));
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\nFoo: Bar\nBar: Bazzel\n") == 0);
+  xfree (buf);
+
+  pkc_delete (pk, pke_next_value (pkc_lookup (pk, "Foo:"), "Foo:"));
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Baz\nBar: Bazzel\n") == 0);
+  xfree (buf);
+
+  pkc_delete (pk, pkc_lookup (pk, "Foo:"));
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Bar: Bazzel\n") == 0);
+  xfree (buf);
+
+  pkc_delete (pk, pkc_first (pk));
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "") == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Foo:", "A really long value spanning across multiple lines"
+	   " that has to be wrapped at a convenient space.");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: A really long value spanning across multiple"
+		  " lines that has to be\n  wrapped at a convenient space.\n")
+	  == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Foo:", "XA really long value spanning across multiple lines"
+	   " that has to be wrapped at a convenient space.");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: XA really long value spanning across multiple"
+		  " lines that has to\n  be wrapped at a convenient space.\n")
+	  == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Foo:", "XXXXA really long value spanning across multiple lines"
+	   " that has to be wrapped at a convenient space.");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: XXXXA really long value spanning across multiple"
+		  " lines that has\n  to be wrapped at a convenient space.\n")
+	  == 0);
+  xfree (buf);
+
+  pkc_set (pk, "Foo:", "Areallylongvaluespanningacrossmultiplelines"
+	   "thathastobewrappedataconvenientspacethatisnotthere.");
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Foo: Areallylongvaluespanningacrossmultiplelinesthat"
+		  "hastobewrappedataco\n nvenientspacethatisnotthere.\n")
+	  == 0);
+  xfree (buf);
+  pkc_release (pk);
+
+  pk = pkc_new ();
+  assert (pk);
+
+  err = gcry_sexp_build (&key, NULL, "(hello world)");
+  assert (err == 0);
+  assert (key);
+
+  err = pkc_set_private_key (pk, key);
+  gcry_sexp_release (key);
+  assert (err == 0);
+  buf = pkc_to_string (pk);
+  assert (strcmp (buf, "Key: (hello world)\n") == 0);
+  xfree (buf);
+  pkc_release (pk);
+}
+
+
+void
+convert (const char *fname)
+{
+  gpg_error_t err;
+  estream_t source;
+  gcry_sexp_t key;
+  char *buf;
+  size_t buflen;
+  gpgrt_ssize_t nread;
+  struct stat st;
+  pkc_t pk;
+
+  source = es_fopen (fname, "rb");
+  if (source == NULL)
+    goto leave;
+
+  if (fstat (es_fileno (source), &st))
+    goto leave;
+
+  buflen = st.st_size;
+  buf = xtrymalloc (buflen+1);
+  assert (buf);
+
+  if (es_fread (buf, buflen, 1, source) != 1)
+    goto leave;
+
+  err = gcry_sexp_sscan (&key, NULL, buf, buflen);
+  if (err)
+    {
+      fprintf (stderr, "malformed s-expression in %s\n", fname);
+      exit (1);
+    }
+
+  pk = pkc_new ();
+  assert (pk);
+
+  err = pkc_set_private_key (pk, key);
+  assert (err == 0);
+
+  err = pkc_write (pk, es_stdout);
+  assert (err == 0);
+
+  return;
+
+ leave:
+  perror (fname);
+  exit (1);
+}
+
+
+void
+parse (const char *fname)
+{
+  gpg_error_t err;
+  estream_t source;
+  char *buf;
+  pkc_t pk_a, pk_b;
+  pke_t e;
+  int line;
+
+  source = es_fopen (fname, "rb");
+  if (source == NULL)
+    {
+      perror (fname);
+      exit (1);
+    }
+
+  err = pkc_parse (&pk_a, &line, source);
+  if (err)
+    {
+      fprintf (stderr, "failed to parse %s line %d: %s\n",
+	       fname, line, gpg_strerror (err));
+      exit (1);
+    }
+
+  buf = pkc_to_string (pk_a);
+  xfree (buf);
+
+  pk_b = pkc_new ();
+  assert (pk_b);
+
+  for (e = pkc_first (pk_a); e; e = pke_next (e))
+    {
+      gcry_sexp_t key = NULL;
+
+      if (strcasecmp (pke_name (e), "Key:") == 0)
+	{
+	  err = pkc_get_private_key (pk_a, &key);
+	  if (err)
+	    key = NULL;
+	}
+
+      if (key)
+	{
+	  err = pkc_set_private_key (pk_b, key);
+	  assert (err == 0);
+	}
+      else
+	{
+	  err = pkc_add (pk_b, pke_name (e), pke_value (e));
+	  assert (err == 0);
+	}
+    }
+
+    buf = pkc_to_string (pk_b);
+    if (verbose)
+      fprintf (stdout, "%s", buf);
+    xfree (buf);
+}
+
+
+void
+print_usage (void)
+{
+  fprintf (stderr,
+	   "usage: t-private-keys [--verbose]"
+	   " [--convert <private-key-file>"
+	   " || --parse <extended-private-key-file>]\n");
+  exit (2);
+}
+
+
+int
+main (int argc, char **argv)
+{
+  enum { TEST, CONVERT, PARSE } command = TEST;
+
+  if (argc)
+    { argc--; argv++; }
+  if (argc && !strcmp (argv[0], "--verbose"))
+    {
+      verbose = 1;
+      argc--; argv++;
+    }
+
+  if (argc && !strcmp (argv[0], "--convert"))
+    {
+      command = CONVERT;
+      argc--; argv++;
+      if (argc != 1)
+	print_usage ();
+    }
+
+  if (argc && !strcmp (argv[0], "--parse"))
+    {
+      command = PARSE;
+      argc--; argv++;
+      if (argc != 1)
+	print_usage ();
+    }
+
+  switch (command)
+    {
+    case TEST:
+      run_tests ();
+      run_modification_tests ();
+      break;
+
+    case CONVERT:
+      convert (*argv);
+      break;
+
+    case PARSE:
+      parse (*argv);
+      break;
+    }
+
+  return 0;
+}
diff --git a/common/util.h b/common/util.h
index 6410b11..466c519 100644
--- a/common/util.h
+++ b/common/util.h
@@ -333,6 +333,9 @@ int _gnupg_isatty (int fd);
 /*-- Macros to replace ctype ones to avoid locale problems. --*/
 #define spacep(p)   (*(p) == ' ' || *(p) == '\t')
 #define digitp(p)   (*(p) >= '0' && *(p) <= '9')
+#define alphap(p)   ((*(p) >= 'A' && *(p) <= 'Z')       \
+                     || (*(p) >= 'a' && *(p) <= 'z'))
+#define alnump(p)   (alphap (p) || digitp (p))
 #define hexdigitp(a) (digitp (a)                     \
                       || (*(a) >= 'A' && *(a) <= 'F')  \
                       || (*(a) >= 'a' && *(a) <= 'f'))
diff --git a/tests/migrations/Makefile.am b/tests/migrations/Makefile.am
index a592bdd..0f581c2 100644
--- a/tests/migrations/Makefile.am
+++ b/tests/migrations/Makefile.am
@@ -28,11 +28,17 @@ AM_CFLAGS =
 
 TESTS_ENVIRONMENT = GPG_AGENT_INFO= LC_ALL=C
 
-TESTS = from-classic.test
+TESTS = from-classic.test \
+	extended-private-key-format.test
 
 TEST_FILES = from-classic.gpghome/pubring.gpg.asc \
 	     from-classic.gpghome/secring.gpg.asc \
-	     from-classic.gpghome/trustdb.gpg.asc
+	     from-classic.gpghome/trustdb.gpg.asc \
+	     extended-private-key-format.gpghome/trustdb.gpg.asc \
+	     extended-private-key-format.gpghome/pubring.kbx.asc \
+	     extended-private-key-format.gpghome/private-keys-v1.d/13FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc \
+	     extended-private-key-format.gpghome/private-keys-v1.d/343D8AF79796EE107D645A2787A9D9252F924E6F.key.asc \
+	     extended-private-key-format.gpghome/private-keys-v1.d/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc
 
 EXTRA_DIST = $(TESTS) $(TEST_FILES)
 
diff --git a/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/13FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/13FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc
new file mode 100644
index 0000000..d9192b1
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/13FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc
@@ -0,0 +1,27 @@
+-----BEGIN PGP ARMORED FILE-----
+Version: GnuPG v2
+Comment: Use "gpg --dearmor" for unpacking
+
+S2V5OiAocHJpdmF0ZS1rZXkgKHJzYSAobiAjMDBBODUyNTY3NkVDRTRENzVGRTZE
+MDA3M0YyQkY5OUE2RjQ5MzNDRUJERDQKIDUyOEFGNTZFNEM2MUUyRjczMTI0NzM5
+MzY0NUREQUY1OEVBREQ1NjUyOUMyNTM5Nzc4MjM2NDYzREYyRDQ1ODUyMEU4MEUK
+IDM0QzA1ODI0MkIzRkY4OEREMzlBODgzQjQ3NUI2NkNFQUFCQkM5OTg5RkYwMUZG
+RTczNzY2MEU5QjYxQkI5REM5MTIwNUQKIDQyOEZGRkU4RjY3NUZBRUY2MTM2NThD
+NzJEQTZENzUwQzBFQkM0MEFGNjIzRDIwNjY5MkM4MjUxNEM0MDREODgyNUFCNzAK
+IDEwMDEjKShlICMwMTAxIykoZCAjMDBCQ0EwMDE0NDg1RkI3NkQ1MEU5QjZDQkE1
+NzIxQUMxMTIxMzkwRjg2MDhENDA4NEIKIEQwNDVBODc2REYzODEwRjExNEJDMkQ2
+OEVCNTUyRTYxQjAxRURCQzI0ODFGMDhDODI4MzJFMDBFMjc5RDY3QTg1MzA1NUQK
+IENBRTVDMjM1Njg1MUNCRTM2RDYxMEM0RDJBQjQzRkE2NTU5ODVDNDQ2OUQxRDkx
+MUUxQUZEODE3RUFBNUZFRTBGRjI2NTcKIDRDMzU5RTE3NTI4NzA1MjE5NDUzQjUx
+QUVDMTBEQkY3NTYyQjA2MUQ1QzY2QzM1QkIzRjlGMEIyMjJCOUQxOTZCOSMpKHAK
+ICAjMDBDMzNDNTgwNjM5OTZCRDU5NzUyQUFCREZEQUNEQUE3QjRCNjZBQTE3NTRF
+RTBEODlCNzc5NEYwREU4RkY3MjRDNTQKIDlGRjExMkEzMzI5MkJCOUQ3QkNFRTc5
+NEYwODAyNEMzRTU1RkQ4MjMzRjUwNzlFRDQ5OTFDNERGMjYxOEQ5IykocSAjMDAK
+IERDQjU5NDVGMDBGMUFGNDM4QkQ0QzMxMUI4QkFDQTNEOURCMEFEMTY1MTk4NjUz
+NDIwMzBGMURGMzA1N0U1NTMyQzQ3RjUKIDhEMzMwM0NCQTNDOEEyOTgxNEY2MTdC
+N0IzREVFOThGQUFBQUVFODExQjQ5OEZBQUYyMTc3Qjc3NjkjKSh1ICMyOUZCMkQK
+IEY2OUIyMzVBNDlBOTA2QjEwRUY3RDhGODFBQUVBOEFEODFFN0NEREUxRjRBNzlD
+RTI0NEJGOEZDRTZERDVFQjE4MTFCMEIKIEQ1RTUxNjVCOTU3MDg1MDM2OTAxREQy
+ODVBNjI4QzI5N0E3ODJEQTgxNTczQTQzRDFDMDkjKSkpCg==
+=laTh
+-----END PGP ARMORED FILE-----
diff --git a/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/343D8AF79796EE107D645A2787A9D9252F924E6F.key.asc b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/343D8AF79796EE107D645A2787A9D9252F924E6F.key.asc
new file mode 100644
index 0000000..1eede1c
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/343D8AF79796EE107D645A2787A9D9252F924E6F.key.asc
@@ -0,0 +1,17 @@
+-----BEGIN PGP ARMORED FILE-----
+Version: GnuPG v2
+Comment: Use "gpg --dearmor" for unpacking
+
+KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToArHGqWD0rP0Nn/c3nYELTD4m1
+gqR7f2+l1ZUMdHcweYwn/fVjaJKmbR+9GzeHWP398FWYs5mCU1DIfrZLF0nJnAJ6
+WRnN9TL+oub1BqqLvCmDSngRuZZ2gUX8DVmD8xTsPnDnG74QDUnvtnpDIAs32sg5
+dnusstrriXD8xXgt0g8pKDE6cTIxOgC449htJbbp5rkJHvBDs4YxEIkk5ykoMTpn
+MTI4Ol+ITxpSMOT5R67Bu4XWoYU7nVeYURpb6LJ8LK2CV7ygECwFdRFdukiGFB+a
+TP8nF6xtuXalaBuerkKp4QXVKqOIkp7MWN2TAOOg9eERHPT//whryf49meNYMPLv
+KAe60udHY76Glm+Zso+24WnEwXX2od1PHVV3CItWRb7YmhgGKSgxOnkxMjg6AgXt
+40h2lpiIHTjbu6fiCBzbr5j2eQX3cNoydkRphJ66bqD+DsPW/Ag0WBCQxgRaLgMr
+db64fQT+fyjbTBLbC8ytt5hpCbm/q5x3TTXDAUNjoB3CnA/tQItBy7qqq/A0d3FZ
+grr6AixK58uZ4wauy8LRZCph67UZ8akcgwJkmVkpKDE6eDIwOn/Y1rjZASGMK9IG
+b1y/ZDKT0zkTKSkp
+=muRa
+-----END PGP ARMORED FILE-----
diff --git a/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc
new file mode 100644
index 0000000..7083673
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc
@@ -0,0 +1,20 @@
+-----BEGIN PGP ARMORED FILE-----
+Version: GnuPG v2
+Comment: Use "gpg --dearmor" for unpacking
+
+S2V5OiAocHJpdmF0ZS1rZXkgKGVsZyAocCAjMDBDQ0Q4QjFGOURBQzc0RDgwOEND
+NTJGMEQ4OTQ2NERBNTU0QzY5RDY3RjMKIDMyM0M0MkE5NUM5OTYyREY0MjEyNkVD
+MEUwOTcxRjQ5QjgxMTUyOUE2QTJBRTlGMEFERUI4MzlBNjM0NjE1Q0Q1NkZBNTQK
+IEY1QTBCN0VGMjVBMEUyRkU4NDNGQTJFNkUwMjFDQUI0MTE5RTYwMzk0QzlENkEz
+RjdBRDRGNTc3OTZEMzY2NjlBNTEyNjYKIEMyN0E4RDFDNUE2QjQxNDFENUM4MzFF
+ODQ1NDFGM0M4MTFFODkwNzg5ODAzMzgyOTVGODJCN0Y3RkQ0MzMzRUZEOTMzMTIK
+IEYyQUIjKShnICMwNiMpKHkgIzM3NzNBNkQ5RUM4ODlENzZFMzI0RDZFNUVDMjFC
+RDQ1Njk5ODMxQUU0RkQwQUUwMzc4MjAKIDVCQUU1QjhDRTg1RkFEQUJEN0U2QjdD
+NzMwMjVDQjNENzMwRDVDNTgyOTAzNEQ3NkJFMDg1NUMyRTlGRjdBNDkyM0VGRkEK
+IEYxNkE5NjY2OTQ0REJDNjI5NDgzOEZDM0YwOUZGOTY0QThEMDIzQ0I4RUJBMzMy
+RkIwNTFFQTAyODIwRUU2MTIwRkZCRTYKIDJCMzZBMjAyQjNDNzUyRjlEQTc2QjJF
+QzExQTY3RDJFMzVFNjZFQzEwNjM1ODdCMjI1MDBFOEE0NkQxNTdCNzUjKSh4ICMK
+IDY5MTVDNkNFRDI1ODE0M0Y4OTM3QjEzMzVGNDg4N0YwMDQyQjdDNjMwMDUzOThG
+OTM5NkJCODUzMjM4Q0I2IykpKQo=
+=6fkh
+-----END PGP ARMORED FILE-----
diff --git a/tests/migrations/extended-private-key-format.gpghome/pubring.kbx.asc b/tests/migrations/extended-private-key-format.gpghome/pubring.kbx.asc
new file mode 100644
index 0000000..5012371
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.gpghome/pubring.kbx.asc
@@ -0,0 +1,39 @@
+-----BEGIN PGP ARMORED FILE-----
+Version: GnuPG v2
+Comment: Use "gpg --dearmor" for unpacking
+
+AAAAIAEBAAJLQlhmAAAAAFcYtiNXGLYjAAAAAAAAAAAAAAQXAgEAAAAAAH4AAAOF
+AAIAHMHeuzTqi3EAnq+kdJc9UOHED97PAAAAIAAAAADNPQ9XAcv8rLKkkHMFo3iH
+snkHqgAAADwAAAAAAAAAAQAMAAACJQAAACIAAAAAAAIABP//////////AAAAAAAA
+AAAAAAAAVxi2IwAAAACZAaIEP/JSaxEEAKxxqlg9Kz9DZ/3N52BC0w+JtYKke39v
+pdWVDHR3MHmMJ/31Y2iSpm0fvRs3h1j9/fBVmLOZglNQyH62SxdJyZwCelkZzfUy
+/qLm9Qaqi7wpg0p4EbmWdoFF/A1Zg/MU7D5w5xu+EA1J77Z6QyALN9rIOXZ7rLLa
+64lw/MV4LdIPAKC449htJbbp5rkJHvBDs4YxEIkk5wP/X4hPGlIw5PlHrsG7hdah
+hTudV5hRGlvosnwsrYJXvKAQLAV1EV26SIYUH5pM/ycXrG25dqVoG56uQqnhBdUq
+o4iSnsxY3ZMA46D14REc9P//CGvJ/j2Z41gw8u8oB7rS50djvoaWb5myj7bhacTB
+dfah3U8dVXcIi1ZFvtiaGAYD+gIF7eNIdpaYiB0427un4ggc26+Y9nkF93DaMnZE
+aYSeum6g/g7D1vwINFgQkMYEWi4DK3W+uH0E/n8o20wS2wvMrbeYaQm5v6ucd001
+wwFDY6AdwpwP7UCLQcu6qqvwNHdxWYK6+gIsSufLmeMGrsvC0WQqYeu1GfGpHIMC
+ZJlZtCJUZXN0IHR3byAobm8gcHApIDx0d29AZXhhbXBsZS5jb20+iF8EExECAB8F
+Aj/yUmsCGwMHCwkIBwMCAQMVAgMDFgIBAh4BAheAAAoJEJc9UOHED97PgEMAn0F8
+RGDrnmXv7rqM2+pic2oDz1kpAJ0SWPHxdjJHWzoGMrHqocAy/3wFi7kBDQQ/8lJv
+EAQAzNix+drHTYCMxS8NiUZNpVTGnWfzMjxCqVyZYt9CEm7A4JcfSbgRUppqKunw
+reuDmmNGFc1W+lT1oLfvJaDi/oQ/oubgIcq0EZ5gOUydaj961PV3ltNmaaUSZsJ6
+jRxaa0FB1cgx6EVB88gR6JB4mAM4KV+Ct/f9QzPv2TMS8qsAAwYD/jdzptnsiJ12
+4yTW5ewhvUVpmDGuT9CuA3ggW65bjOhfravX5rfHMCXLPXMNXFgpA012vghVwun/
+ekkj7/rxapZmlE28YpSDj8Pwn/lkqNAjy466My+wUeoCgg7mEg/75is2ogKzx1L5
+2nay7BGmfS415m7BBjWHsiUA6KRtFXt1iEkEGBECAAkFAj/yUm8CGwwACgkQlz1Q
+4cQP3s8svgCgmWcpVwvtDN3nAVT1dMFTvCz0hfwAoI4VszJBesG/8GyLW+e2E+Li
+QXVqciq2GGJ3Ap2KvoCwCL/DhCAfcGsAAAHgAgEAAAAAAF4AAAFuAAEAHM8jSQsP
+eLhQu7xzadEgtibsq/UdAAAAIAAAAAAAAAABAAwAAADvAAAAJgAAAAAAAQAE////
+/wAAAAAAAAAAAAAAAFcYtkkAAAAAmQCMBD/yU70BBACoUlZ27OTXX+bQBz8r+Zpv
+STPOvdRSivVuTGHi9zEkc5NkXdr1jq3VZSnCU5d4I2Rj3y1FhSDoDjTAWCQrP/iN
+05qIO0dbZs6qu8mYn/Af/nN2YOm2G7nckSBdQo//6PZ1+u9hNljHLabXUMDrxAr2
+I9IGaSyCUUxATYglq3AQAQAJAQG0JlRlc3QgdGhyZWUgKG5vIHBwKSA8dGhyZWVA
+ZXhhbXBsZS5jb20+iLUEEwECAB8FAj/yU70CGwMHCwkIBwMCAQMVAgMDFgIBAh4B
+AheAAAoJENEgtibsq/UdakMD/2wg19VhpNbtM5CiVif1V57h945OmXr5Lh2SAsI5
+agMb9XXuT9yXsmv+JD5hEE6LRL98XAwGfvaQS9062aJQCocZAWdPJeEEsu+pMn/I
+QdHqGdkr7Oy6xjwSa+gh19JMg4mqR4AIQSkKvRoTSqSAGbi+gytnTmkA7aEUltog
+dYeJLGB5MYPnSPwADYVfNtLxsKZESLA=
+=tULv
+-----END PGP ARMORED FILE-----
diff --git a/tests/migrations/extended-private-key-format.gpghome/trustdb.gpg.asc b/tests/migrations/extended-private-key-format.gpghome/trustdb.gpg.asc
new file mode 100644
index 0000000..f4d354d
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.gpghome/trustdb.gpg.asc
@@ -0,0 +1,31 @@
+-----BEGIN PGP ARMORED FILE-----
+Version: GnuPG v2
+Comment: Use "gpg --dearmor" for unpacking
+
+AWdwZwMDAQUBAgAAVxi2IwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQoAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+=eBUi
+-----END PGP ARMORED FILE-----
diff --git a/tests/migrations/extended-private-key-format.test b/tests/migrations/extended-private-key-format.test
new file mode 100755
index 0000000..9c373e8
--- /dev/null
+++ b/tests/migrations/extended-private-key-format.test
@@ -0,0 +1,57 @@
+#!/bin/sh
+# Copyright 2016 g10 Code GmbH
+#
+# This file is free software; as a special exception the author gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.  This file is
+# distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY, to the extent permitted by law; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+if [ -z "$srcdir" ]; then
+   echo "not called from make" >&2
+   exit 1
+fi
+
+unset GNUPGHOME
+set -e
+
+# (We may not use a relative name for gpg-agent.)
+GPG_AGENT="$(cd ../../agent && /bin/pwd)/gpg-agent"
+GPG="../../g10/gpg --no-permission-warning --no-greeting --no-secmem-warning
+--batch --agent-program=${GPG_AGENT}|--debug-quick-random"
+
+TEST="extended-private-key-format"
+
+setup_home()
+{
+    XGNUPGHOME="`mktemp -d`"
+    mkdir -p "$XGNUPGHOME/private-keys-v1.d"
+    for F in $srcdir/$TEST.gpghome/*.asc; do
+	$GPG --dearmor <"$F" >"$XGNUPGHOME/`basename $F .asc`"
+    done
+    for F in $srcdir/$TEST.gpghome/private-keys-v1.d/*.asc; do
+	$GPG --dearmor <"$F" >"$XGNUPGHOME/private-keys-v1.d/`basename $F .asc`"
+    done
+    chmod go-rwx $XGNUPGHOME/* $XGNUPGHOME/*/*
+    export GNUPGHOME="$XGNUPGHOME"
+}
+
+cleanup_home()
+{
+    rm -rf -- "$XGNUPGHOME"
+}
+
+assert_keys_usable()
+{
+    for KEY in C40FDECF ECABF51D; do
+	$GPG --list-secret-keys $KEY >/dev/null
+    done
+}
+
+setup_home
+assert_keys_usable
+cleanup_home
+
+
+# XXX try changing a key, and check that the format is not changed.

commit c6d1f2f08c68efe7e80887219064a8ce6365128f
Author: Justus Winter <justus at g10code.com>
Date:   Thu Apr 21 12:59:59 2016 +0200

    common: Add 'free_strlist_wipe' which wipes memory.
    
    * common/strlist.c (free_strlist_wipe): New function.
    * common/strlist.h (free_strlist_wipe): New prototype.
    
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/common/strlist.c b/common/strlist.c
index 2ba0209..319d034 100644
--- a/common/strlist.c
+++ b/common/strlist.c
@@ -39,6 +39,7 @@
 #include "common-defs.h"
 #include "strlist.h"
 #include "utf8conv.h"
+#include "mischelp.h"
 
 void
 free_strlist( strlist_t sl )
@@ -52,6 +53,19 @@ free_strlist( strlist_t sl )
 }
 
 
+void
+free_strlist_wipe (strlist_t sl)
+{
+    strlist_t sl2;
+
+    for(; sl; sl = sl2 ) {
+	sl2 = sl->next;
+        wipememory (sl, sizeof *sl + strlen (sl->d));
+	xfree(sl);
+    }
+}
+
+
 /* Add STRING to the LIST at the front.  This function terminates the
    process on memory shortage.  */
 strlist_t
diff --git a/common/strlist.h b/common/strlist.h
index 94dd32f..45f5543 100644
--- a/common/strlist.h
+++ b/common/strlist.h
@@ -40,6 +40,8 @@ struct string_list
 typedef struct string_list *strlist_t;
 
 void    free_strlist (strlist_t sl);
+void	free_strlist_wipe (strlist_t sl);
+
 strlist_t add_to_strlist (strlist_t *list, const char *string);
 strlist_t add_to_strlist_try (strlist_t *list, const char *string);
 

commit 95303ee11df12f284e98d02dba993eda9e425383
Author: Justus Winter <justus at g10code.com>
Date:   Thu Apr 21 12:36:04 2016 +0200

    common: Add 'append_to_strlist_try' which can fail.
    
    * common/strlist.c (append_to_strlist): Use the new function.
    (append_to_strlist_try): New function.
    * common/strlist.h (append_to_strlist_try): New prototype.
    
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/common/strlist.c b/common/strlist.c
index 760a460..2ba0209 100644
--- a/common/strlist.c
+++ b/common/strlist.c
@@ -112,9 +112,24 @@ add_to_strlist2( strlist_t *list, const char *string, int is_utf8 )
 strlist_t
 append_to_strlist( strlist_t *list, const char *string )
 {
+  strlist_t sl;
+  sl = append_to_strlist_try (list, string);
+  if (sl == NULL)
+    abort ();
+  return sl;
+}
+
+
+/* Add STRING to the LIST at the end.  */
+strlist_t
+append_to_strlist_try (strlist_t *list, const char *string)
+{
     strlist_t r, sl;
 
     sl = xmalloc( sizeof *sl + strlen(string));
+    if (sl == NULL)
+      return NULL;
+
     sl->flags = 0;
     strcpy(sl->d, string);
     sl->next = NULL;
diff --git a/common/strlist.h b/common/strlist.h
index acb92f7..94dd32f 100644
--- a/common/strlist.h
+++ b/common/strlist.h
@@ -46,6 +46,7 @@ strlist_t add_to_strlist_try (strlist_t *list, const char *string);
 strlist_t add_to_strlist2( strlist_t *list, const char *string, int is_utf8);
 
 strlist_t append_to_strlist (strlist_t *list, const char *string);
+strlist_t append_to_strlist_try (strlist_t *list, const char *string);
 strlist_t append_to_strlist2 (strlist_t *list, const char *string,
                               int is_utf8);
 

commit 342cc488890241b41e49f50886617115342721d6
Author: Justus Winter <justus at g10code.com>
Date:   Wed Apr 13 14:25:30 2016 +0200

    agent: Convert key format document to org.
    
    * agent/keyformat.txt: Convert to org mode.
    
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/agent/keyformat.txt b/agent/keyformat.txt
index e760412..5e15ecf 100644
--- a/agent/keyformat.txt
+++ b/agent/keyformat.txt
@@ -1,11 +1,11 @@
-keyformat.txt (wk 2001-12-18)
------------------------------
+keyformat.txt               emacs, please switch to -*- org -*- mode
+-------------
 
 
 Some notes on the format of the secret keys used with gpg-agent.
 
-Location of keys
-================
+* Location of keys
+
 The secret keys[1] are stored on a per file basis in a directory below
 the ~/.gnupg home directory.  This directory is named
 
@@ -16,9 +16,8 @@ and should have permissions 700.
 The secret keys are stored in files with a name matching the
 hexadecimal representation of the keygrip[2] and suffixed with ".key".
 
+* Unprotected Private Key Format
 
-Unprotected Private Key Format
-==============================
 The content of the file is an S-Expression like the ones used with
 Libgcrypt.  Here is an example of an unprotected file:
 
@@ -43,9 +42,8 @@ optional but required for some operations to calculate the fingerprint
 of the key.  This timestamp should be a string with the number of
 seconds since Epoch or an ISO time string (yyyymmddThhmmss).
 
+* Protected Private Key Format
 
-Protected Private Key Format
-==============================
 A protected key is like this:
 
 (protected-private-key
@@ -69,7 +67,7 @@ optional; the isotimestamp is 15 bytes long (e.g. "19610711T172000").
 
 The currently defined protection modes are:
 
-1. openpgp-s2k3-sha1-aes-cbc
+** openpgp-s2k3-sha1-aes-cbc
 
   This describes an algorithm using using AES in CBC mode for
   encryption, SHA-1 for integrity protection and the String to Key
@@ -118,7 +116,7 @@ The currently defined protection modes are:
   the stored one - If they don't match the integrity of the key is not
   given.
 
-2. openpgp-s2k3-ocb-aes
+** openpgp-s2k3-ocb-aes
 
   This describes an algorithm using using AES-128 in OCB mode, a nonce
   of 96 bit, a taglen of 128 bit, and the String to Key algorithm 3
@@ -156,8 +154,7 @@ The currently defined protection modes are:
    (protected-at "18950523T000000")
   )
 
-
-3. openpgp-native
+** openpgp-native
 
   This is a wrapper around the OpenPGP Private Key Transport format
   which resembles the standard OpenPGP format and allows the use of an
@@ -194,10 +191,8 @@ The currently defined protection modes are:
    (uri http://foo.bar x-foo:whatever_you_want)
    (comment whatever))
 
+* Shadowed Private Key Format
 
-
-Shadowed Private Key Format
-============================
 To keep track of keys stored on IC cards we use a third format for
 private kyes which are called shadow keys as they are only a reference
 to keys stored on a token:
@@ -224,9 +219,7 @@ readers don't allow passing a variable length PIN.
 
 More items may be added to the list.
 
-
-OpenPGP Private Key Transfer Format
-===================================
+* OpenPGP Private Key Transfer Format
 
 This format is used to transfer keys between gpg and gpg-agent.
 
@@ -239,28 +232,26 @@ This format is used to transfer keys between gpg and gpg-agent.
   (protection PROTTYPE PROTALGO IV S2KMODE S2KHASH S2KSALT S2KCOUNT))
 
 
-* V is the packet version number (3 or 4).
-* PUBKEYALGO is a Libgcrypt algo name
-* CURVENAME is the name of the curve - only used with ECC.
-* P1 .. PN are the parameters; the public parameters are never encrypted
-  the secrect key parameters are encrypted if the "protection" list is
-  given.  To make this more explicit each parameter is preceded by a
-  flag "_" for cleartext or "e" for encrypted text.
-* CSUM is the deprecated 16 bit checksum as defined by OpenPGP.  This
-  is an optional element.
-* If PROTTYPE is "sha1" the new style SHA1 checksum is used if it is "sum"
-  the old 16 bit checksum (above) is used and if it is "none" no
-  protection at all is used.
-* PROTALGO is a Libgcrypt style cipher algorithm name
-* IV is the initialization verctor.
-* S2KMODE is the value from RFC-4880.
-* S2KHASH is a a libgcrypt style hash algorithm identifier.
-* S2KSALT is the 8 byte salt
-* S2KCOUNT is the count value from RFC-4880.
-
-
-Persistent Passphrase Format
-============================
+ * V is the packet version number (3 or 4).
+ * PUBKEYALGO is a Libgcrypt algo name
+ * CURVENAME is the name of the curve - only used with ECC.
+ * P1 .. PN are the parameters; the public parameters are never encrypted
+   the secrect key parameters are encrypted if the "protection" list is
+   given.  To make this more explicit each parameter is preceded by a
+   flag "_" for cleartext or "e" for encrypted text.
+ * CSUM is the deprecated 16 bit checksum as defined by OpenPGP.  This
+   is an optional element.
+ * If PROTTYPE is "sha1" the new style SHA1 checksum is used if it is "sum"
+   the old 16 bit checksum (above) is used and if it is "none" no
+   protection at all is used.
+ * PROTALGO is a Libgcrypt style cipher algorithm name
+ * IV is the initialization verctor.
+ * S2KMODE is the value from RFC-4880.
+ * S2KHASH is a a libgcrypt style hash algorithm identifier.
+ * S2KSALT is the 8 byte salt
+ * S2KCOUNT is the count value from RFC-4880.
+
+* Persistent Passphrase Format
 
 Note: That this has not yet been implemented.
 
@@ -355,14 +346,8 @@ hashed:
     (protected-at "20100915T111722")
    )
 
+* Notes
 
-
-
-
-
-
-Notes:
-======
 [1] I usually use the terms private and secret key exchangeable but prefer the
 term secret key because it can be visually be better distinguished
 from the term public key.

commit 0c35e09278514f1e3377a4b0a9b1f44dd39b1bf4
Author: Justus Winter <justus at g10code.com>
Date:   Thu Apr 21 14:36:21 2016 +0200

    tests: Make migration test more robust and silent.
    
    * tests/migrations/from-classic.test: Fix in-tree build, silence test.
    
    Fixes-commit: defbc70b
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/tests/migrations/from-classic.test b/tests/migrations/from-classic.test
index a61a5c3..9b81d45 100755
--- a/tests/migrations/from-classic.test
+++ b/tests/migrations/from-classic.test
@@ -13,6 +13,7 @@ if [ -z "$srcdir" ]; then
    exit 1
 fi
 
+unset GNUPGHOME
 set -e
 
 # (We may not use a relative name for gpg-agent.)
@@ -20,22 +21,28 @@ GPG_AGENT="$(cd ../../agent && /bin/pwd)/gpg-agent"
 GPG="../../g10/gpg --no-permission-warning --no-greeting --no-secmem-warning
 --batch --agent-program=${GPG_AGENT}|--debug-quick-random"
 
-export GNUPGHOME="from-classic.gpghome"
+TEST="from-classic"
 
 setup_home()
 {
-    rm -rf -- "$GNUPGHOME"
-    mkdir "$GNUPGHOME"
-    for F in $srcdir/$GNUPGHOME/*.asc
-    do
-	$GPG --dearmor <"$F" >"$GNUPGHOME/`echo $F | sed -e 's/....$//'`"
+    XGNUPGHOME="`mktemp -d`"
+    rm -rf -- scratch
+    mkdir -p "$XGNUPGHOME"
+    for F in $srcdir/$TEST.gpghome/*.asc; do
+	$GPG --dearmor <"$F" >"$XGNUPGHOME/`basename $F .asc`"
     done
-    chmod go-rwx $GNUPGHOME/*
+    chmod go-rwx $XGNUPGHOME/*
+    export GNUPGHOME="$XGNUPGHOME"
+}
+
+cleanup_home()
+{
+    rm -rf -- "$XGNUPGHOME"
 }
 
 trigger_migration()
 {
-    $GPG --list-secret-keys >/dev/null
+    $GPG --list-secret-keys >/dev/null 2>&1
 }
 
 assert_migrated()
@@ -50,12 +57,14 @@ assert_migrated()
 setup_home
 trigger_migration
 assert_migrated
+cleanup_home
 
 # Test with an existing private-keys-v1.d.
 setup_home
 mkdir "$GNUPGHOME/private-keys-v1.d"
 trigger_migration
 assert_migrated
+cleanup_home
 
 # Test with an existing private-keys-v1.d with weird permissions.
 setup_home
@@ -63,5 +72,6 @@ mkdir "$GNUPGHOME/private-keys-v1.d"
 chmod 0 "$GNUPGHOME/private-keys-v1.d"
 trigger_migration
 assert_migrated
+cleanup_home
 
 # XXX Check a case where the migration fails.

-----------------------------------------------------------------------

Summary of changes:
 agent/findkey.c                                    | 161 ++++-
 agent/keyformat.txt                                | 143 ++--
 common/Makefile.am                                 |   7 +-
 common/private-keys.c                              | 740 +++++++++++++++++++++
 common/private-keys.h                              | 109 +++
 common/strlist.c                                   |  29 +
 common/strlist.h                                   |   3 +
 common/t-private-keys.c                            | 543 +++++++++++++++
 common/util.h                                      |   3 +
 tests/migrations/Makefile.am                       |  10 +-
 ...3FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc |  27 +
 ...3D8AF79796EE107D645A2787A9D9252F924E6F.key.asc} |   2 +-
 ...B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc |  20 +
 .../pubring.kbx.asc                                |  39 ++
 .../trustdb.gpg.asc                                |   4 +-
 ...assic.test => extended-private-key-format.test} |  46 +-
 tests/migrations/from-classic.test                 |  26 +-
 17 files changed, 1821 insertions(+), 91 deletions(-)
 create mode 100644 common/private-keys.c
 create mode 100644 common/private-keys.h
 create mode 100644 common/t-private-keys.c
 create mode 100644 tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/13FDB8809B17C5547779F9D205C45F47CE0217CE.key.asc
 copy tests/{openpgp/privkeys/343D8AF79796EE107D645A2787A9D9252F924E6F.asc => migrations/extended-private-key-format.gpghome/private-keys-v1.d/343D8AF79796EE107D645A2787A9D9252F924E6F.key.asc} (95%)
 create mode 100644 tests/migrations/extended-private-key-format.gpghome/private-keys-v1.d/8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34.key.asc
 create mode 100644 tests/migrations/extended-private-key-format.gpghome/pubring.kbx.asc
 copy tests/migrations/{from-classic.gpghome => extended-private-key-format.gpghome}/trustdb.gpg.asc (96%)
 copy tests/migrations/{from-classic.test => extended-private-key-format.test} (52%)


hooks/post-receive
-- 
The GNU Privacy Guard
http://git.gnupg.org




More information about the Gnupg-commits mailing list