Major openpgp2ssh overhaul. It's an unforgiving and brittle tool, but
[monkeysphere.git] / src / keytrans / openpgp2ssh.c
index d6bac685399c090749c50eeb33e80b4ad735b6ce..30e19d5e1790f1c42305afb604a55c62a56a1b75 100644 (file)
@@ -3,47 +3,47 @@
 #include <gnutls/openpgp.h>
 #include <gnutls/x509.h>
 
+/* for waitpid() */
+#include <sys/types.h>
+#include <sys/wait.h>
+
 /* 
    Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
-   Date: Tue, 01 Apr 2008
+   Date: 2008-06-12 13:47:41-0400
    License: GPL v3 or later
 
-   monkeysphere private key translator: execute this with an GPG
-   secret key on stdin (at the moment, only passphraseless RSA keys
-   work).
+   monkeysphere key translator: execute this with an OpenPGP key on
+   stdin, (please indicate the specific keyid that you want as the
+   first argument if there are subkeys).  At the moment, only public
+   keys and passphraseless secret keys work.
 
-   It will spit out a PEM-encoded version of the key on stdout, which
-   can be fed into ssh-add like this:
+   For secret keys, it will spit out a PEM-encoded version of the key
+   on stdout, which can be fed into ssh-add like this:
 
-    gpg --export-secret-keys $KEYID | monkeysphere | ssh-add -c /dev/stdin
+    gpg --export-secret-keys $KEYID | openpgp2ssh $KEYID | ssh-add -c /dev/stdin
 
-   Requirements: I've only built this so far with GnuTLS v2.3.4 --
-   version 2.2.0 does not contain the appropriate pieces.
+   For public keys, it will spit out a single line of text that can
+   (with some massaging) be used in an openssh known_hosts or
+   authorized_keys file.  For example:
 
-   Notes: gpgkey2ssh doesn't seem to provide the same public
-   keys. Mighty weird!
+    echo server.example.org $(gpg --export $KEYID | openpgp2ssh $KEYID) >> ~/.ssh/known_hosts
 
-0 wt215@squeak:~/monkeysphere$ gpg --export-secret-keys 1DCDF89F | ~dkg/src/monkeysphere/monkeysphere  | ssh-add -c /dev/stdin
-gnutls version: 2.3.4
-OpenPGP RSA Key, with 1024 bits
-Identity added: /dev/stdin (/dev/stdin)
-The user has to confirm each use of the key
-0 wt215@squeak:~/monkeysphere$ ssh-add -L
-ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9gWQqfrnhQKDQnND/3eOexpddE64J+1zp9fcyCje7H5LKclb6DBV2HS6WgW32PJhIzvP+fYZM3dzXea3fpv14y1SicXiRBDgF9SnsNA1qWn2RyzkLcKy7PmM0PDYtU1oiLTcQj/xkWcqW2sLKHT/WW+vZP5XP7RMGN/yWNMfE2Q== /dev/stdin
-0 wt215@squeak:~/monkeysphere$ gpgkey2ssh 1DCDF89F
-ssh-rsa AAAAB3NzaC1yc2EAAACBAL2BZCp+ueFAoNCc0P/d457Gl10Trgn7XOn19zIKN7sfkspyVvoMFXYdLpaBbfY8mEjO8/59hkzd3Nd5rd+m/XjLVKJxeJEEOAX1Kew0DWpafZHLOQtwrLs+YzQ8Ni1TWiItNxCP/GRZypbawsodP9Zb69k/lc/tEwY3/JY0x8TZAAAAAwEAAQ== COMMENT
-0 wt215@squeak:~/monkeysphere$ 
+   Requirements: I've only built this so far with GnuTLS v2.3.x.
+   GnuTLS 2.2.x does not contain the appropriate functionality.
 
  */
 
 
-int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) {
-  gnutls_openpgp_privkey_t pgp_privkey;
+/* FIXME: keyid should be const as well */
+int convert_private_pgp_to_x509(gnutls_x509_privkey_t* output, const gnutls_openpgp_privkey_t* pgp_privkey, gnutls_openpgp_keyid_t* keyid) {
   gnutls_datum_t m, e, d, p, q, u, g, y, x;
   gnutls_pk_algorithm_t pgp_algo;
   unsigned int pgp_bits;
   int ret;
 
+/* FIXME: actually respect keyid argument.  At the moment, we just
+   emit the primary key.  */
+
   init_datum(&m);
   init_datum(&e);
   init_datum(&d);
@@ -54,34 +54,14 @@ int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) {
   init_datum(&y);
   init_datum(&x);
 
-  if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) {
-    err("Failed to initialized OpenPGP private key (error: %d)\n", ret);
-    return 1;
-  }
-
-
-  /* format could be either: GNUTLS_OPENPGP_FMT_RAW,
-     GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW,
-     otherwise, use BASE64: */
-
-  if (getenv("MONKEYSPHERE_RAW")) {
-    err("assuming RAW formatted private keys\n");
-    if (ret = gnutls_openpgp_privkey_import(pgp_privkey, input, GNUTLS_OPENPGP_FMT_RAW, NULL, 0), ret)
-      err("failed to import the OpenPGP private key in RAW format (error: %d)\n", ret);
-  } else {
-    err("assuming BASE64 formatted private keys\n");
-    if (ret = gnutls_openpgp_privkey_import (pgp_privkey, input, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0), ret)
-      err("failed to import the OpenPGP private key in BASE64 format (error: %d)\n", ret);
-  }
-
-  pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(pgp_privkey, &pgp_bits);
+  pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(*pgp_privkey, &pgp_bits);
   if (pgp_algo < 0) {
     err("failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo);
     return 1;
   }
   if (pgp_algo == GNUTLS_PK_RSA) {
     err("OpenPGP RSA Key, with %d bits\n", pgp_bits);
-    ret = gnutls_openpgp_privkey_export_rsa_raw(pgp_privkey, &m, &e, &d, &p, &q, &u);
+    ret = gnutls_openpgp_privkey_export_rsa_raw(*pgp_privkey, &m, &e, &d, &p, &q, &u);
     if (GNUTLS_E_SUCCESS != ret) {
       err ("failed to export RSA key parameters (error: %d)\n", ret);
       return 1;
@@ -94,7 +74,7 @@ int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) {
     }
   } else if (pgp_algo == GNUTLS_PK_DSA) {
     err("OpenPGP DSA Key, with %d bits\n", pgp_bits);
-    ret = gnutls_openpgp_privkey_export_dsa_raw(pgp_privkey, &p, &q, &g, &y, &x);
+    ret = gnutls_openpgp_privkey_export_dsa_raw(*pgp_privkey, &p, &q, &g, &y, &x);
     if (GNUTLS_E_SUCCESS != ret) {
       err ("failed to export DSA key parameters (error: %d)\n", ret);
       return 1;
@@ -116,10 +96,190 @@ int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) {
     return 1; 
   }
 
-  gnutls_openpgp_privkey_deinit(pgp_privkey);
   return 0;
 }
 
+/* FIXME: keyid should be const also */
+int emit_public_openssh_from_pgp(const gnutls_openpgp_crt_t* pgp_crt, gnutls_openpgp_keyid_t* keyid) {
+  gnutls_openpgp_keyid_t curkeyid;
+  int ret;
+  int subkeyidx;
+  int subkeycount;
+  int found = 0;
+  gnutls_datum_t m, e, p, q, g, y, algolabel;
+  unsigned int bits;
+  gnutls_pk_algorithm_t algo;
+  const gnutls_datum_t* all[5];
+  const char* algoname;
+  int mpicount;
+  /* output_data must be at least 2 chars longer than the maximum possible
+     algorithm name: */
+  char output_data[20];
+
+  /* variables for the output conversion: */
+  int pipestatus;
+  int pipefd, child_pid;
+  char* const b64args[] = {"/usr/bin/base64", "--wrap=0", NULL};
+
+  init_datum(&m);
+  init_datum(&e);
+  init_datum(&p);
+  init_datum(&q);
+  init_datum(&g);
+  init_datum(&algolabel);
+
+
+  /* figure out if we've got the right thing: */
+  subkeycount = gnutls_openpgp_crt_get_subkey_count(*pgp_crt);
+  if (subkeycount < 0) {
+    err("Could not determine subkey count (got value %d)\n", subkeycount);
+    return 1;
+  }
+
+  if ((keyid == NULL) && 
+      (subkeycount > 0)) {
+    err("No keyid passed in, but there were %d keys to choose from\n", subkeycount + 1);
+    return 1;
+  }
+
+  if (keyid != NULL) {
+    ret = gnutls_openpgp_crt_get_key_id(*pgp_crt, curkeyid);
+    if (ret) {
+      err("Could not get keyid (error: %d)\n", ret);
+      return 1;
+    }
+  }
+  if ((keyid == NULL) || (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0)) {
+    /* we want to export the primary key: */
+    err("exporting primary key\n");
+
+    /* FIXME: this is almost identical to the block below for subkeys.
+       This clumsiness seems inherent in the gnutls OpenPGP API,
+       though.  ugh. */
+    algo = gnutls_openpgp_crt_get_pk_algorithm(*pgp_crt, &bits);
+    if (algo < 0) {
+      err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo);
+      return algo;
+    } else if (algo == GNUTLS_PK_RSA) {
+      err("OpenPGP RSA certificate, with %d bits\n", bits);
+      ret = gnutls_openpgp_crt_get_pk_rsa_raw(*pgp_crt, &m, &e);
+      if (GNUTLS_E_SUCCESS != ret) {
+       err ("failed to export RSA key parameters (error: %d)\n", ret);
+       return 1;
+      }
+    } else if (algo == GNUTLS_PK_DSA) {
+      err("OpenPGP DSA Key, with %d bits\n", bits);
+      ret = gnutls_openpgp_crt_get_pk_dsa_raw(*pgp_crt, &p, &q, &g, &y);
+      if (GNUTLS_E_SUCCESS != ret) {
+       err ("failed to export DSA key parameters (error: %d)\n", ret);
+       return 1;
+      }
+    }
+    found = 1;
+
+  } else {
+    /* lets trawl through the subkeys until we find the one we want: */
+    for (subkeyidx = 0; (subkeyidx < subkeycount) && !found; subkeyidx++) {
+      ret = gnutls_openpgp_crt_get_subkey_id(*pgp_crt, subkeyidx, curkeyid);
+      if (ret) {
+       err("Could not get keyid of subkey with index %d (error: %d)\n", subkeyidx, ret);
+       return 1;
+      }
+      if (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0) {
+       err("exporting subkey index %d\n", subkeyidx);
+
+       /* FIXME: this is almost identical to the block above for the
+          primary key. */
+       algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(*pgp_crt, subkeyidx, &bits);
+       if (algo < 0) {
+         err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo);
+         return algo;
+       } else if (algo == GNUTLS_PK_RSA) {
+         err("OpenPGP RSA certificate, with %d bits\n", bits);
+         ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(*pgp_crt, subkeyidx, &m, &e);
+         if (GNUTLS_E_SUCCESS != ret) {
+           err ("failed to export RSA key parameters (error: %d)\n", ret);
+           return 1;
+         }
+       } else if (algo == GNUTLS_PK_DSA) {
+         err("OpenPGP DSA Key, with %d bits\n", bits);
+         ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(*pgp_crt, subkeyidx, &p, &q, &g, &y);
+         if (GNUTLS_E_SUCCESS != ret) {
+           err ("failed to export DSA key parameters (error: %d)\n", ret);
+           return 1;
+         }
+       }
+       found = 1;
+       
+      }
+    }
+  }
+
+  if (!found) {
+    err("Could not find key in input\n");
+    return 1;
+  }
+
+  /* if we made it this far, we've got MPIs, and we've got the
+     algorithm, so we just need to emit the info */
+  if (algo == GNUTLS_PK_RSA) {
+    algoname = "ssh-rsa";
+    mpicount = 3;
+
+    all[0] = &algolabel;
+    all[1] = &e;
+    all[2] = &m;
+  } else if (algo == GNUTLS_PK_DSA) {
+    algoname = "ssh-dss";
+    mpicount = 5;
+
+    all[0] = &algolabel;
+    all[1] = &p;
+    all[2] = &q;
+    all[3] = &g;
+    all[4] = &y;
+  } else {
+    err("Key algorithm was neither DSA nor RSA (it was %d).  Can't deal.  Sorry!\n", algo);
+    return 1;
+  }
+
+  if (ret = datum_from_string(&algolabel, algoname), ret) {
+    err("couldn't label string (error: %d)\n", ret);
+    return ret;
+  }
+
+  snprintf(output_data, sizeof(output_data), "%s ", algoname);
+
+  pipefd = create_writing_pipe(&child_pid, b64args[0], b64args);
+  if (pipefd < 0) {
+    err("failed to create a writing pipe (returned %d)\n", pipefd);
+    return pipefd;
+  }
+    
+  write(1, output_data, strlen(output_data));
+
+  if (0 != write_data_fd_with_length(pipefd, all, mpicount)) {
+    err("was not able to write out RSA key data\n");
+    return 1;
+  }
+  close(pipefd);
+  if (child_pid != waitpid(child_pid, &pipestatus, 0)) {
+    err("could not wait for child process to return for some reason.\n");
+    return 1;
+  }
+  if (pipestatus != 0) {
+    err("base64 pipe died with return code %d\n", pipestatus);
+    return pipestatus;
+  }
+
+  write(1, "\n", 1);
+
+  
+  return 0;
+}
+
+
+
 int convert_x509_to_pgp(gnutls_openpgp_privkey_t* output, gnutls_datum_t* input) {
   gnutls_x509_privkey_t x509_privkey;
   gnutls_datum_t m, e, d, p, q, u, g, y, x;
@@ -203,69 +363,88 @@ int main(int argc, char* argv[]) {
   gnutls_datum_t data;
   int ret;
   gnutls_x509_privkey_t x509_privkey;
+  gnutls_openpgp_privkey_t pgp_privkey;
+  gnutls_openpgp_crt_t pgp_crt;
 
   char output_data[10240];
   size_t ods = sizeof(output_data);
+  
+  gnutls_openpgp_keyid_t keyid;
+  gnutls_openpgp_keyid_t* use_keyid;
 
   init_gnutls();
+
+  /* figure out what keyid we should be looking for: */
+  use_keyid = NULL;
+  if (argv[1] != NULL) {
+    ret = convert_string_to_keyid(keyid, argv[1]);
+    if (ret != 0)
+      return ret;
+    use_keyid = &keyid;
+  }
+
   
   init_datum(&data);
 
-  /* slurp in the private key from stdin */
+  /* slurp in the key from stdin */
   if (ret = set_datum_fd(&data, 0), ret) {
     err("didn't read file descriptor 0\n");
     return 1;
   }
 
 
-
-  /* Or, instead, read in key from a file name: 
-  if (ret = set_datum_file(&data, argv[1]), ret) {
-    err("didn't read file '%s'\n", argv[1]);
+  if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) {
+    err("Failed to initialized OpenPGP private key (error: %d)\n", ret);
     return 1;
   }
-*/
-
-  /* treat the passed file as an X.509 private key, and extract its
-     component values: */
-
-/*   if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { */
-/*     err("Failed to import the X.509 key (error: %d)\n", ret); */
-/*     return 1; */
-/*   } */
-/*   gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); */
-
-  /* try to print the PEM-encoded private key: */
-/*   ret = gnutls_x509_privkey_export (x509_privkey, */
-/*                                 GNUTLS_X509_FMT_PEM, */
-/*                                 output_data, */
-/*                                 &ods); */
-/*   printf("ret: %u; ods: %u;\n", ret, ods); */
-/*   if (ret == 0) { */
-/*     write(0, output_data, ods); */
-/*   } */
-
-
-  if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) {
-    err("Failed to initialize X.509 private key (error: %d)\n", ret);
-    return 1;
+  /* check whether it's a private key or a public key, by trying them: */
+  if ((gnutls_openpgp_privkey_import(pgp_privkey, &data, GNUTLS_OPENPGP_FMT_RAW, NULL, 0) == 0) || 
+      (gnutls_openpgp_privkey_import(pgp_privkey, &data, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0) == 0)) {
+    /* we're dealing with a private key */
+    err("Translating private key\n");
+    if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) {
+      err("Failed to initialize X.509 private key for output (error: %d)\n", ret);
+      return 1;
+    }
+    
+    ret = convert_private_pgp_to_x509(&x509_privkey, &pgp_privkey, use_keyid);
+
+    gnutls_openpgp_privkey_deinit(pgp_privkey);
+    if (ret)
+      return ret;
+
+    ret = gnutls_x509_privkey_export (x509_privkey,
+                                     GNUTLS_X509_FMT_PEM,
+                                     output_data,
+                                     &ods);
+    if (ret == 0) {
+      write(1, output_data, ods);
+    }
+    gnutls_x509_privkey_deinit(x509_privkey);
+  
+  } else {
+    if (ret = gnutls_openpgp_crt_init(&pgp_crt), ret) {
+      err("Failed to initialized OpenPGP certificate (error: %d)\n", ret);
+      return 1;
+    }
+    
+    if ((gnutls_openpgp_crt_import(pgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW) == 0) || 
+       (gnutls_openpgp_crt_import(pgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64) == 0)) {
+      /* we're dealing with a public key */
+      err("Translating public key\n");
+
+      ret = emit_public_openssh_from_pgp(&pgp_crt, use_keyid);
+      
+    } else {
+      /* we have no idea what kind of key this is at all anyway! */
+      err("Input does contain any form of OpenPGP key I recognize.");
+      return 1;
+    }
   }
 
-  if (ret = convert_pgp_to_x509(&x509_privkey, &data), ret) {
-    return ret;
-  }
 
-  ret = gnutls_x509_privkey_export (x509_privkey,
-                                   GNUTLS_X509_FMT_PEM,
-                                   output_data,
-                                   &ods);
-  printf("ret: %u; ods: %u;\n", ret, ods);
-  if (ret == 0) {
-    write(1, output_data, ods);
-  }
 
 
-  gnutls_x509_privkey_deinit(x509_privkey);
   gnutls_global_deinit();
   return 0;
 }