From afe5fcda52e88438c7a7278117b2e03f510a9c1c Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Mon, 20 Dec 2021 17:15:14 +0100 Subject: [PATCH] gpg: Add unfinished code for --export-secret-ssh-key. * g10/gpg.c (exportSecretSshKey): New. (opts): Add --export-secret-ssh-key. (main): Implement option. * g10/export.c (do_export_stream): Factor keywrap key code out to ... (get_keywrap_key): new. (mb_write_uint32, mb_write_uint8) (mb_write_data, mb_write_cstring) (mb_write_string, mb_write_mpi): New. (receive_raw_seckey_from_agent): New. (export_secret_ssh_key): New. -- Due to time constraints the code is not yet ready. --- g10/export.c | 469 +++++++++++++++++++++++++++++++++++++++++++++++---- g10/gpg.c | 14 ++ g10/main.h | 1 + 3 files changed, 449 insertions(+), 35 deletions(-) diff --git a/g10/export.c b/g10/export.c index c7cfcfaa4..1eec70935 100644 --- a/g10/export.c +++ b/g10/export.c @@ -1872,6 +1872,49 @@ do_export_one_keyblock (ctrl_t ctrl, kbnode_t keyblock, u32 *keyid, } +/* For secret key export we need to setup a decryption context. + * Returns 0 and the context at r_cipherhd. */ +static gpg_error_t +get_keywrap_key (ctrl_t ctrl, gcry_cipher_hd_t *r_cipherhd) +{ +#ifdef ENABLE_SELINUX_HACKS + (void)ctrl; + *r_cipherhd = NULL; + log_error (_("exporting secret keys not allowed\n")); + return gpg_error (GPG_ERR_NOT_SUPPORTED); +#else + gpg_error_t err; + void *kek = NULL; + size_t keklen; + gcry_cipher_hd_t cipherhd; + + *r_cipherhd = NULL; + + err = agent_keywrap_key (ctrl, 1, &kek, &keklen); + if (err) + { + log_error ("error getting the KEK: %s\n", gpg_strerror (err)); + return err; + } + + err = gcry_cipher_open (&cipherhd, GCRY_CIPHER_AES128, + GCRY_CIPHER_MODE_AESWRAP, 0); + if (!err) + err = gcry_cipher_setkey (cipherhd, kek, keklen); + if (err) + log_error ("error setting up an encryption context: %s\n", + gpg_strerror (err)); + + if (!err) + *r_cipherhd = cipherhd; + else + gcry_cipher_close (cipherhd); + xfree (kek); + return err; +#endif +} + + /* Export the keys identified by the list of strings in USERS to the stream OUT. If SECRET is false public keys will be exported. With secret true secret keys will be exported; in this case 1 means the @@ -1945,42 +1988,9 @@ do_export_stream (ctrl_t ctrl, iobuf_t out, strlist_t users, int secret, this we need an extra flag to enable this feature. */ } -#ifdef ENABLE_SELINUX_HACKS - if (secret) - { - log_error (_("exporting secret keys not allowed\n")); - err = gpg_error (GPG_ERR_NOT_SUPPORTED); - goto leave; - } -#endif - /* For secret key export we need to setup a decryption context. */ - if (secret) - { - void *kek = NULL; - size_t keklen; - - err = agent_keywrap_key (ctrl, 1, &kek, &keklen); - if (err) - { - log_error ("error getting the KEK: %s\n", gpg_strerror (err)); - goto leave; - } - - /* Prepare a cipher context. */ - err = gcry_cipher_open (&cipherhd, GCRY_CIPHER_AES128, - GCRY_CIPHER_MODE_AESWRAP, 0); - if (!err) - err = gcry_cipher_setkey (cipherhd, kek, keklen); - if (err) - { - log_error ("error setting up an encryption context: %s\n", - gpg_strerror (err)); - goto leave; - } - xfree (kek); - kek = NULL; - } + if (secret && (err = get_keywrap_key (ctrl, &cipherhd))) + goto leave; for (;;) { @@ -2121,6 +2131,87 @@ do_export_stream (ctrl_t ctrl, iobuf_t out, strlist_t users, int secret, } + +/* Write the uint32 VALUE to MB in networ byte order. */ +static void +mb_write_uint32 (membuf_t *mb, u32 value) +{ + unsigned char buffer[4]; + + ulongtobuf (buffer, (ulong)value); + put_membuf (mb, buffer, 4); +} + +/* Write the byte C to MB. */ +static void +mb_write_uint8 (membuf_t *mb, int c) +{ + unsigned char buf[1]; + + buf[0] = c; + put_membuf (mb, buf, 1); +} + + +/* Simple wrapper around put_membuf. */ +static void +mb_write_data (membuf_t *mb, const void *data, size_t datalen) +{ + put_membuf (mb, data, datalen); +} + +/* Write STRING with terminating Nul to MB. */ +static void +mb_write_cstring (membuf_t *mb, const char *string) +{ + put_membuf (mb, string, strlen (string)+1); +} + +/* Write an SSH style string to MB. */ +static void +mb_write_string (membuf_t *mb, const void *string, size_t n) +{ + mb_write_uint32 (mb, (u32)n); + mb_write_data (mb, string, n); +} + +/* Write an MPI as SSH style string to MB */ +static void +mb_write_mpi (membuf_t *mb, gcry_mpi_t mpi, int strip_prefix) +{ + unsigned int nbits; + const unsigned char *p; + size_t n; + + if (gcry_mpi_get_flag (mpi, GCRYMPI_FLAG_OPAQUE)) + { + p = gcry_mpi_get_opaque (mpi, &nbits); + n = (nbits + 7) / 8; + + if (strip_prefix && n > 1 && p[0] == 0x40) + { + /* We need to strip our 0x40 prefix. */ + p++; + n--; + } + mb_write_string (mb, p, n); + } + else + { + gpg_error_t err; + unsigned char *buf; + + err = gcry_mpi_aprint (GCRYMPI_FMT_SSH, &buf, &n, mpi); + if (err) + set_membuf_err (mb, err); + else + { + mb_write_data (mb, buf, n); + gcry_free (buf); + } + } +} + static gpg_error_t @@ -2139,6 +2230,7 @@ key_to_sshblob (membuf_t *mb, const char *identifier, ...) put_membuf (mb, identifier, buflen); if (buflen > 11 && !memcmp (identifier, "ecdsa-sha2-", 11)) { + /* Store the name of the curve taken from the identifier. */ ulongtobuf (nbuf, (ulong)(buflen - 11)); put_membuf (mb, nbuf, 4); put_membuf (mb, identifier+11, buflen - 11); @@ -2502,3 +2594,310 @@ export_ssh_key (ctrl_t ctrl, const char *userid) release_kbnode (keyblock); return err; } + + +/* Simplified version of receive_seckey_from_agent used to get the raw + * key. */ +gpg_error_t +receive_raw_seckey_from_agent (ctrl_t ctrl, gcry_cipher_hd_t cipherhd, + const char *hexgrip, gcry_sexp_t *r_key) +{ + gpg_error_t err = 0; + unsigned char *wrappedkey = NULL; + size_t wrappedkeylen; + unsigned char *key = NULL; + size_t keylen, realkeylen; + gcry_sexp_t s_skey = NULL; + + *r_key = NULL; + if (opt.verbose) + log_info ("key %s: asking agent for the secret parts\n", hexgrip); + + { + char * prompt = gpg_format_keydesc (ctrl, NULL, FORMAT_KEYDESC_KEYGRIP, 1); + err = agent_export_key (ctrl, hexgrip, prompt, 0, NULL, + &wrappedkey, &wrappedkeylen, + NULL, NULL, 0); + xfree (prompt); + } + if (err) + goto leave; + + if (wrappedkeylen < 24) + { + err = gpg_error (GPG_ERR_INV_LENGTH); + goto leave; + } + keylen = wrappedkeylen - 8; + key = xtrymalloc_secure (keylen); + if (!key) + { + err = gpg_error_from_syserror (); + goto leave; + } + err = gcry_cipher_decrypt (cipherhd, key, keylen, wrappedkey, wrappedkeylen); + if (err) + goto leave; + realkeylen = gcry_sexp_canon_len (key, keylen, NULL, &err); + if (!realkeylen) + goto leave; /* Invalid csexp. */ + + err = gcry_sexp_sscan (&s_skey, NULL, key, realkeylen); + if (!err) + { + gcry_log_debugsxp ("skey", s_skey); + *r_key = s_skey; + s_skey = NULL; + } + + leave: + gcry_sexp_release (s_skey); + xfree (key); + xfree (wrappedkey); + if (err) + { + log_error ("key %s: error receiving key from agent:" + " %s%s\n", hexgrip, gpg_strerror (err), + ""); + } + return err; +} + + +/* Export the key identified by USERID in the SSH secret key format. + * The USERID must be given in keygrip format (prefixed with a '&') + * and thus no OpenPGP key is required. The exported key is not + * protected. */ +gpg_error_t +export_secret_ssh_key (ctrl_t ctrl, const char *userid) +{ + gpg_error_t err; + KEYDB_SEARCH_DESC desc; + estream_t fp = NULL; + const char *fname = "-"; + gcry_cipher_hd_t cipherhd = NULL; + char hexgrip[KEYGRIP_LEN * 2 + 1]; + gcry_sexp_t skey = NULL; + gcry_sexp_t skeyalgo = NULL; + const char *identifier = NULL; + membuf_t mb; + membuf_t mb2; + void *blob = NULL; + size_t bloblen; + const char *s; + size_t n; + char *p; + int pkalgo; + int i; + gcry_mpi_t keyparam[10] = { NULL }; + struct b64state b64_state; + + init_membuf_secure (&mb, 1024); + init_membuf_secure (&mb2, 1024); + + /* Check that a keygrip has been given. */ + err = classify_user_id (userid, &desc, 1); + if (err || desc.mode != KEYDB_SEARCH_MODE_KEYGRIP ) + { + log_error (_("key \"%s\" not found: %s\n"), userid, + err? gpg_strerror (err) : "Not a Keygrip" ); + return err; + } + + bin2hex (desc.u.grip, KEYGRIP_LEN, hexgrip); + + if ((err = get_keywrap_key (ctrl, &cipherhd))) + goto leave; + + err = receive_raw_seckey_from_agent (ctrl, cipherhd, hexgrip, &skey); + if (err) + goto leave; + + /* Get the type of the key expression. */ + s = gcry_sexp_nth_data (skey, 0, &n); + if (!s || !(n == 11 && !memcmp (s, "private-key", 11))) + { + log_info ("Note: only on-disk secret keys may be exported\n"); + err = gpg_error (GPG_ERR_NO_SECKEY); + goto leave; + } + + mb_write_cstring (&mb, "openssh-key-v1"); /* Auth_Magic. */ + mb_write_string (&mb, "none", 4); /* ciphername */ + mb_write_string (&mb, "none", 4); /* kdfname */ + mb_write_uint32 (&mb, 0); /* kdfoptions */ + mb_write_uint32 (&mb, 1); /* number of keys */ + + pkalgo = get_pk_algo_from_key (skey); + switch (pkalgo) + { + case PUBKEY_ALGO_RSA: + case PUBKEY_ALGO_RSA_S: + identifier = "ssh-rsa"; + err = gcry_sexp_extract_param (skey, NULL, "nedpq", + &keyparam[0], + &keyparam[1], + &keyparam[2], + &keyparam[3], + &keyparam[4], + NULL); + if (err) + goto leave; + mb_write_string (&mb2, identifier, strlen (identifier)); + mb_write_mpi (&mb2, keyparam[1], 0); /* e (right, e is first here) */ + mb_write_mpi (&mb2, keyparam[0], 0); /* n */ + /* Append public to the output block as an SSH string. */ + p = get_membuf (&mb2, &n); + if (!p) + { + err = gpg_error_from_syserror (); + goto leave; + } + mb_write_string (&mb, p, n); + xfree (p); + init_membuf_secure (&mb2, 1024); + mb_write_string (&mb2, identifier, strlen (identifier)); + { + char checkbytes[4]; + gcry_create_nonce (checkbytes, sizeof checkbytes); + mb_write_data (&mb2, checkbytes, sizeof checkbytes); + mb_write_data (&mb2, checkbytes, sizeof checkbytes); + } + mb_write_mpi (&mb2, keyparam[0], 0); /* n */ + mb_write_mpi (&mb2, keyparam[1], 0); /* e */ + /*FIXME: Fixup u,p,q to match the OpenSSH format. */ + mb_write_mpi (&mb2, keyparam[2], 0); /* d */ + mb_write_mpi (&mb2, keyparam[1], 0); /* iqmp1 */ + mb_write_mpi (&mb2, keyparam[3], 0); /* p */ + mb_write_mpi (&mb2, keyparam[4], 0); /* q */ + /* Fixme: take the comment from skey. */ + mb_write_string (&mb2, "", 9); + /* Pad to a blocksize of 8 (for cipher "none"). */ + i = 0; + while (peek_membuf (&mb2, &n) && (n % 8)) + mb_write_uint8 (&mb2, ++i); + /* Append encrypted block to the output as an SSH string. */ + p = get_membuf (&mb2, &n); + if (!p) + { + err = gpg_error_from_syserror (); + goto leave; + } + mb_write_string (&mb, p, n); + xfree (p); + err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); + 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"; */ + + /* 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])) */ + /* { */ + /* identifier = "ssh-ed25519"; */ + /* err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); */ + /* } */ + /* else if (openpgp_oid_is_ed448 (pk->pkey[0])) */ + /* { */ + /* identifier = "ssh-ed448"; */ + /* err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); */ + /* } */ + /* else */ + /* err = gpg_error (GPG_ERR_UNKNOWN_CURVE); */ + /* break; */ + + case PUBKEY_ALGO_DSA: + log_info ("Note: export of ssh-dsa keys is not supported\n"); + err = gpg_error (GPG_ERR_NOT_SUPPORTED); + break; + + case PUBKEY_ALGO_ELGAMAL_E: + case PUBKEY_ALGO_ELGAMAL: + err = gpg_error (GPG_ERR_UNUSABLE_SECKEY); + break; + + default: + err = GPG_ERR_PUBKEY_ALGO; + break; + } + + if (err) + goto leave; + + blob = get_membuf (&mb, &bloblen); + if (!blob) + { + err = gpg_error_from_syserror (); + 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; + } + + err = b64enc_start_es (&b64_state, fp, "OPENSSH PRIVATE_KEY"); + if (err) + goto leave; + err = b64enc_write (&b64_state, blob, bloblen); + b64enc_finish (&b64_state); + if (err) + goto leave; + + if (es_ferror (fp)) + err = gpg_error_from_syserror (); + else + { + if (fp != es_stdout && es_fclose (fp)) + err = gpg_error_from_syserror (); + fp = NULL; + } + + log_info ("Beware: the private key is not protected;" + " use \"ssh-keygen -p\" to protect it\n"); + if (err) + log_error (_("error writing '%s': %s\n"), fname, gpg_strerror (err)); + + + leave: + xfree (blob); + gcry_sexp_release (skey); + gcry_sexp_release (skeyalgo); + gcry_cipher_close (cipherhd); + xfree (get_membuf (&mb2, NULL)); + xfree (get_membuf (&mb, NULL)); + if (fp != es_stdout) + es_fclose (fp); + return err; +} diff --git a/g10/gpg.c b/g10/gpg.c index 00dbf5283..8c1aa96d4 100644 --- a/g10/gpg.c +++ b/g10/gpg.c @@ -158,6 +158,7 @@ enum cmd_and_opt_values aExportSecret, aExportSecretSub, aExportSshKey, + aExportSecretSshKey, aCheckKeys, aGenRevoke, aDesigRevoke, @@ -527,6 +528,7 @@ static gpgrt_opt_t opts[] = { ARGPARSE_c (aExportSecret, "export-secret-keys" , "@" ), ARGPARSE_c (aExportSecretSub, "export-secret-subkeys" , "@" ), ARGPARSE_c (aExportSshKey, "export-ssh-key", "@" ), + ARGPARSE_c (aExportSecretSshKey, "export-secret-ssh-key", "@" ), ARGPARSE_c (aImport, "import", N_("import/merge keys")), ARGPARSE_c (aFastImport, "fast-import", "@"), #ifdef ENABLE_CARD_SUPPORT @@ -2676,6 +2678,7 @@ main (int argc, char **argv) case aExportSecret: case aExportSecretSub: case aExportSshKey: + case aExportSecretSshKey: case aSym: case aClearsign: case aGenRevoke: @@ -4893,6 +4896,17 @@ main (int argc, char **argv) } break; + case aExportSecretSshKey: + if (argc != 1) + wrong_args ("--export-secret-ssh-key "); + rc = export_secret_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 05ec8c26a..e5308744a 100644 --- a/g10/main.h +++ b/g10/main.h @@ -443,6 +443,7 @@ gpg_error_t write_keyblock_to_output (kbnode_t keyblock, int with_armor, unsigned int options); gpg_error_t export_ssh_key (ctrl_t ctrl, const char *userid); +gpg_error_t export_secret_ssh_key (ctrl_t ctrl, const char *userid); /*-- dearmor.c --*/ int dearmor_file( const char *fname );