[PATCH 3/3] common: Add support for the new extended private key format.

Justus Winter justus at g10code.com
Thu Apr 14 16:41:12 CEST 2016


* 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.

--
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>
---
 agent/findkey.c         | 161 +++++++++++-
 agent/keyformat.txt     |  78 +++++-
 common/Makefile.am      |   7 +-
 common/private-keys.c   | 666 ++++++++++++++++++++++++++++++++++++++++++++++++
 common/private-keys.h   |  99 +++++++
 common/t-private-keys.c | 543 +++++++++++++++++++++++++++++++++++++++
 common/util.h           |   3 +
 7 files changed, 1547 insertions(+), 10 deletions(-)
 create mode 100644 common/private-keys.c
 create mode 100644 common/private-keys.h
 create mode 100644 common/t-private-keys.c

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 9c91336..95dd2bb 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-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
@@ -153,7 +217,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
@@ -181,7 +245,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.
 
@@ -213,7 +277,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
 
 To allow persistent storage of cached passphrases we use a scheme
 similar to the private-key storage format.  This is a master
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..1525f92
--- /dev/null
+++ b/common/private-keys.c
@@ -0,0 +1,666 @@
+/* private-keys.c - Parser and writer for the extended private key format.
+ *	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 <assert.h>
+#include <gcrypt.h>
+#include <gpg-error.h>
+#include <string.h>
+
+#include "private-keys.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);
+  xfree (entry->value);
+  free_strlist (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)
+{
+  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]);
+      append_to_strlist (&entry->raw_value, buf);
+      offset += amount;
+      len -= amount;
+    }
+
+#undef LINELEN
+  return 0;
+}
+
+
+/* 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)
+{
+  assert_value (pke);
+  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.  */
+static gpg_error_t
+_pkc_add (pkc_t pk, char *name, char *value, strlist_t raw_value,
+	  int preserve_order)
+{
+  pke_t e;
+
+  assert (value || raw_value);
+
+  if (name && ! valid_name (name))
+    return gpg_error (GPG_ERR_INV_NAME);
+
+  if (name && strcasecmp (name, "Key:") == 0 && pkc_lookup (pk, "Key:"))
+    return gpg_error (GPG_ERR_INV_NAME);
+
+  e = xtrycalloc (1, sizeof *e);
+  if (e == NULL)
+    return gpg_error_from_syserror ();
+
+  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;
+
+  return 0;
+}
+
+
+/* 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 (e->raw_value);
+      e->raw_value = NULL;
+      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)
+{
+  pke_t e;
+
+  e = pkc_lookup (pk, "Key:");
+  if (e == NULL)
+    return gpg_error (GPG_ERR_MISSING_KEY);
+
+  assert_value (e);
+  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.  */
+	  append_to_strlist (&raw_value, buf);
+	  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;
+	    }
+
+	  append_to_strlist (&raw_value, value);
+	  continue;
+	}
+
+      append_to_strlist (&raw_value, buf);
+    }
+
+  /* Add the final entry.  */
+  if (raw_value)
+    err = _pkc_add (*result, name, NULL, raw_value, 1);
+
+ leave:
+  gpgrt_free (buf);
+
+  return err;
+}
+
+
+/* Write a representation of PK to STREAM.  */
+gpg_error_t
+pkc_write (pkc_t pk, estream_t stream)
+{
+  pke_t entry;
+  strlist_t s;
+
+  for (entry = pk->first; entry; entry = entry->next)
+    {
+      if (entry->name)
+	es_fputs (entry->name, stream);
+
+      assert_raw_value (entry);
+      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..1691f75
--- /dev/null
+++ b/common/private-keys.h
@@ -0,0 +1,99 @@
+/* private-keys.c - Parser and writer for the extended private key format.
+ *	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/>.
+ */
+
+#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'))
-- 
2.1.4




More information about the Gnupg-devel mailing list