From 4970868d8d84d3a64b067e5aafc9f097621758d3 Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Fri, 8 Jan 2016 17:22:32 +0100 Subject: [PATCH] gpg: New command --export-ssh-key * g10/export.c: Include membuf.h and host2net.h. (key_to_sshblob): New. (export_ssh_key): New. * g10/gpg.c (aExportSshKey): New. (opts): Add command. (main): Implement that command. -- GnuPG-bug-id: 2212 I have done only a few tests rights now and the ECDSA curves do not yet work. However ssh-keygen -l accept RSA and ed25519 keys exported using this command. Signed-off-by: Werner Koch --- g10/export.c | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++- g10/gpg.c | 16 ++- g10/main.h | 2 + 3 files changed, 312 insertions(+), 2 deletions(-) diff --git a/g10/export.c b/g10/export.c index 95ddb9d8f..f415c1b7e 100644 --- a/g10/export.c +++ b/g10/export.c @@ -1,7 +1,7 @@ /* export.c - Export keys in the OpenPGP defined format. * Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, * 2005, 2010 Free Software Foundation, Inc. - * Copyright (C) 1998-2015 Werner Koch + * Copyright (C) 1998-2016 Werner Koch * * This file is part of GnuPG. * @@ -34,6 +34,8 @@ #include "util.h" #include "main.h" #include "i18n.h" +#include "membuf.h" +#include "host2net.h" #include "trustdb.h" #include "call-agent.h" @@ -1350,3 +1352,295 @@ do_export_stream (ctrl_t ctrl, iobuf_t out, strlist_t users, int secret, log_info(_("WARNING: nothing exported\n")); return err; } + + + + +static gpg_error_t +key_to_sshblob (membuf_t *mb, const char *identifier, ...) +{ + va_list arg_ptr; + gpg_error_t err = 0; + unsigned char nbuf[4]; + unsigned char *buf; + size_t buflen; + gcry_mpi_t a; + + ulongtobuf (nbuf, (ulong)strlen (identifier)); + put_membuf (mb, nbuf, 4); + put_membuf_str (mb, identifier); + va_start (arg_ptr, identifier); + while ((a = va_arg (arg_ptr, gcry_mpi_t))) + { + err = gcry_mpi_aprint (GCRYMPI_FMT_SSH, &buf, &buflen, a); + if (err) + break; + if (!strcmp (identifier, "ssh-ed25519") + && buflen > 5 && buf[4] == 0x40) + { + /* We need to strip our 0x40 prefix. */ + put_membuf (mb, "\x00\x00\x00\x20", 4); + put_membuf (mb, buf+5, buflen-5); + } + else + put_membuf (mb, buf, buflen); + gcry_free (buf); + } + va_end (arg_ptr); + return err; +} + +/* Export the key identified by USERID in the SSH public key format. + The function exports the latest subkey with Authentication + capability unless the '!' suffix is used to export a specific + key. */ +gpg_error_t +export_ssh_key (ctrl_t ctrl, const char *userid) +{ + gpg_error_t err; + kbnode_t keyblock = NULL; + KEYDB_SEARCH_DESC desc; + u32 latest_date; + u32 curtime = make_timestamp (); + kbnode_t latest_key, node; + PKT_public_key *pk; + const char *identifier; + membuf_t mb; + estream_t fp = NULL; + struct b64state b64_state; + const char *fname = "-"; + + init_membuf (&mb, 4096); + + /* We need to know whether the key has been specified using the + exact syntax ('!' suffix). Thus we need to run a + classify_user_id on our own. */ + err = classify_user_id (userid, &desc, 1); + + /* Get the public key. */ + if (!err) + { + getkey_ctx_t getkeyctx; + + err = get_pubkey_byname (ctrl, &getkeyctx, NULL, userid, &keyblock, + NULL, + 0 /* Only usable keys or given exact. */, + 1 /* No AKL lookup. */); + if (!err) + { + err = getkey_next (getkeyctx, NULL, NULL); + if (!err) + err = gpg_error (GPG_ERR_AMBIGUOUS_NAME); + else if (gpg_err_code (err) == GPG_ERR_NO_PUBKEY) + err = 0; + } + getkey_end (getkeyctx); + } + if (err) + { + log_error (_("key \"%s\" not found: %s\n"), userid, gpg_strerror (err)); + return err; + } + + /* The finish_lookup code in getkey.c does not handle auth keys, + thus we have to duplicate the code here to find the latest + subkey. However, if the key has been found using an exact match + ('!' notation) we use that key without any further checks and + even allow the use of the primary key. */ + latest_date = 0; + latest_key = NULL; + for (node = keyblock; node; node = node->next) + { + if ((node->pkt->pkttype == PKT_PUBLIC_SUBKEY + || node->pkt->pkttype == PKT_PUBLIC_KEY) + && node->pkt->pkt.public_key->flags.exact) + { + latest_key = node; + break; + } + } + if (!latest_key) + { + for (node = keyblock; node; node = node->next) + { + if (node->pkt->pkttype != PKT_PUBLIC_SUBKEY) + continue; + + pk = node->pkt->pkt.public_key; + if (DBG_LOOKUP) + log_debug ("\tchecking subkey %08lX\n", + (ulong) keyid_from_pk (pk, NULL)); + if (!(pk->pubkey_usage & PUBKEY_USAGE_AUTH)) + { + if (DBG_LOOKUP) + log_debug ("\tsubkey not usable for authentication\n"); + continue; + } + if (!pk->flags.valid) + { + if (DBG_LOOKUP) + log_debug ("\tsubkey not valid\n"); + continue; + } + if (pk->flags.revoked) + { + if (DBG_LOOKUP) + log_debug ("\tsubkey has been revoked\n"); + continue; + } + if (pk->has_expired) + { + if (DBG_LOOKUP) + log_debug ("\tsubkey has expired\n"); + continue; + } + if (pk->timestamp > curtime && !opt.ignore_valid_from) + { + if (DBG_LOOKUP) + log_debug ("\tsubkey not yet valid\n"); + continue; + } + if (DBG_LOOKUP) + log_debug ("\tsubkey might be fine\n"); + /* In case a key has a timestamp of 0 set, we make sure that it + is used. A better change would be to compare ">=" but that + might also change the selected keys and is as such a more + intrusive change. */ + if (pk->timestamp > latest_date || (!pk->timestamp && !latest_date)) + { + latest_date = pk->timestamp; + latest_key = node; + } + } + } + + if (!latest_key) + { + err = gpg_error (GPG_ERR_UNUSABLE_PUBKEY); + log_error (_("key \"%s\" not found: %s\n"), userid, gpg_strerror (err)); + goto leave; + } + + pk = latest_key->pkt->pkt.public_key; + if (DBG_LOOKUP) + log_debug ("\tusing key %08lX\n", (ulong) keyid_from_pk (pk, NULL)); + + switch (pk->pubkey_algo) + { + case PUBKEY_ALGO_DSA: + identifier = "ssh-dss"; + err = key_to_sshblob (&mb, identifier, + pk->pkey[0], pk->pkey[1], pk->pkey[2], pk->pkey[3], + NULL); + break; + + case PUBKEY_ALGO_RSA: + case PUBKEY_ALGO_RSA_S: + identifier = "ssh-rsa"; + err = key_to_sshblob (&mb, identifier, pk->pkey[1], pk->pkey[0], NULL); + break; + + case PUBKEY_ALGO_ECDSA: + { + char *curveoid; + const char *curve; + + curveoid = openpgp_oid_to_str (pk->pkey[0]); + if (!curveoid) + err = gpg_error_from_syserror (); + else if (!(curve = openpgp_oid_to_curve (curveoid, 0))) + err = gpg_error (GPG_ERR_UNKNOWN_CURVE); + else + { + if (!strcmp (curve, "nistp256")) + identifier = "ecdsa-sha2-nistp256"; + else if (!strcmp (curve, "nistp384")) + identifier = "ecdsa-sha2-nistp384"; + else if (!strcmp (curve, "nistp521")) + identifier = "ecdsa-sha2-nistp521"; + else + identifier = NULL; + + if (!identifier) + err = gpg_error (GPG_ERR_UNKNOWN_CURVE); + else + err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); + } + xfree (curveoid); + } + break; + + case PUBKEY_ALGO_EDDSA: + if (!openpgp_oid_is_ed25519 (pk->pkey[0])) + err = gpg_error (GPG_ERR_UNKNOWN_CURVE); + else + { + identifier = "ssh-ed25519"; + err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); + } + break; + + case PUBKEY_ALGO_ELGAMAL_E: + case PUBKEY_ALGO_ELGAMAL: + err = gpg_error (GPG_ERR_UNUSABLE_PUBKEY); + break; + + default: + err = GPG_ERR_PUBKEY_ALGO; + break; + } + + if (err) + goto leave; + + if (opt.outfile && *opt.outfile && strcmp (opt.outfile, "-")) + fp = es_fopen ((fname = opt.outfile), "w"); + else + fp = es_stdout; + if (!fp) + { + err = gpg_error_from_syserror (); + log_error (_("error creating '%s': %s\n"), fname, gpg_strerror (err)); + goto leave; + } + + es_fprintf (fp, "%s ", identifier); + err = b64enc_start_es (&b64_state, fp, ""); + if (err) + goto leave; + { + void *blob; + size_t bloblen; + + blob = get_membuf (&mb, &bloblen); + if (!blob) + err = gpg_error_from_syserror (); + else + err = b64enc_write (&b64_state, blob, bloblen); + xfree (blob); + if (err) + goto leave; + } + err = b64enc_finish (&b64_state); + if (err) + goto leave; + es_fprintf (fp, " openpgp:0x%08lX\n", (ulong)keyid_from_pk (pk, NULL)); + + if (es_ferror (fp)) + err = gpg_error_from_syserror (); + else + { + if (es_fclose (fp)) + err = gpg_error_from_syserror (); + fp = NULL; + } + + if (err) + log_error (_("error writing '%s': %s\n"), fname, gpg_strerror (err)); + + leave: + es_fclose (fp); + xfree (get_membuf (&mb, NULL)); + release_kbnode (keyblock); + return err; +} diff --git a/g10/gpg.c b/g10/gpg.c index 9b6a14240..4287bda1a 100644 --- a/g10/gpg.c +++ b/g10/gpg.c @@ -1,6 +1,6 @@ /* gpg.c - The GnuPG utility (main for gpg) * Copyright (C) 1998-2011 Free Software Foundation, Inc. - * Copyright (C) 1997-2014 Werner Koch + * Copyright (C) 1997-2016 Werner Koch * Copyright (C) 2015 g10 Code GmbH * * This file is part of GnuPG. @@ -141,6 +141,7 @@ enum cmd_and_opt_values aExport, aExportSecret, aExportSecretSub, + aExportSshKey, aCheckKeys, aGenRevoke, aDesigRevoke, @@ -453,6 +454,7 @@ static ARGPARSE_OPTS opts[] = { ARGPARSE_c (aFetchKeys, "fetch-keys" , "@" ), ARGPARSE_c (aExportSecret, "export-secret-keys" , "@" ), ARGPARSE_c (aExportSecretSub, "export-secret-subkeys" , "@" ), + ARGPARSE_c (aExportSshKey, "export-ssh-key", "@" ), ARGPARSE_c (aImport, "import", N_("import/merge keys")), ARGPARSE_c (aFastImport, "fast-import", "@"), #ifdef ENABLE_CARD_SUPPORT @@ -2400,6 +2402,7 @@ main (int argc, char **argv) case aListSigs: case aExportSecret: case aExportSecretSub: + case aExportSshKey: case aSym: case aClearsign: case aGenRevoke: @@ -4184,6 +4187,17 @@ main (int argc, char **argv) free_strlist(sl); break; + case aExportSshKey: + if (argc != 1) + wrong_args ("--export-ssh-key "); + rc = export_ssh_key (ctrl, argv[0]); + if (rc) + { + write_status_failure ("export-ssh-key", rc); + log_error (_("export as ssh key failed: %s\n"), gpg_strerror (rc)); + } + break; + case aSearchKeys: sl = NULL; for (; argc; argc--, argv++) diff --git a/g10/main.h b/g10/main.h index 06821722a..503f262b9 100644 --- a/g10/main.h +++ b/g10/main.h @@ -368,6 +368,8 @@ gpg_error_t receive_seckey_from_agent (ctrl_t ctrl, gcry_cipher_hd_t cipherhd, char **cache_nonce_addr, const char *hexgrip, PKT_public_key *pk); +gpg_error_t export_ssh_key (ctrl_t ctrl, const char *userid); + /*-- dearmor.c --*/ int dearmor_file( const char *fname ); int enarmor_file( const char *fname );