diff --git a/cmdgen.c b/cmdgen.c index 37b5cac6..044d86aa 100644 --- a/cmdgen.c +++ b/cmdgen.c @@ -137,6 +137,8 @@ void help(void) " -L equivalent to `-O public-openssh'\n" " -p equivalent to `-O public'\n" " --dump equivalent to `-O text'\n" + " --certificate file incorporate a certificate into the key\n" + " --remove-certificate remove any certificate from the key\n" " --reencrypt load a key and save it with fresh " "encryption\n" " --old-passphrase file\n" @@ -250,6 +252,8 @@ int main(int argc, char **argv) char *old_passphrase = NULL, *new_passphrase = NULL; bool load_encrypted; const char *random_device = NULL; + char *certfile = NULL; + bool remove_cert = false; int exit_status = 0; const PrimeGenerationPolicy *primegen = &primegen_probabilistic; bool strong_rsa = false; @@ -392,6 +396,18 @@ int main(int argc, char **argv) } } else if (!strcmp(opt, "-strong-rsa")) { strong_rsa = true; + } else if (!strcmp(opt, "-certificate")) { + if (!val && argc > 1) + --argc, val = *++argv; + if (!val) { + errs = true; + fprintf(stderr, "puttygen: option `-%s'" + " expects an argument\n", opt); + } else { + certfile = val; + } + } else if (!strcmp(opt, "-remove-certificate")) { + remove_cert = true; } else if (!strcmp(opt, "-reencrypt")) { reencrypt = true; } else if (!strcmp(opt, "-ppk-param") || @@ -799,7 +815,8 @@ int main(int argc, char **argv) outfiletmp = dupcat(outfile, ".tmp"); } - if (!change_passphrase && !comment && !reencrypt) { + if (!change_passphrase && !comment && !reencrypt && !certfile && + !remove_cert) { fprintf(stderr, "puttygen: this command would perform no useful" " action\n"); RETURN(1); @@ -849,6 +866,26 @@ int main(int argc, char **argv) RETURN(1); } + /* + * Check consistency properties relating to certificates. + */ + if (certfile && !(sshver == 2 && intype_has_private && + outtype_has_private && infile)) { + fprintf(stderr, "puttygen: certificates can only be added to " + "existing SSH-2 private key files\n"); + RETURN(1); + } + if (remove_cert && !(sshver == 2 && infile)) { + fprintf(stderr, "puttygen: certificates can only be removed from " + "existing SSH-2 key files\n"); + RETURN(1); + } + if (certfile && remove_cert) { + fprintf(stderr, "puttygen: cannot both add and remove a " + "certificate\n"); + RETURN(1); + } + /* ------------------------------------------------------------------ * Now we're ready to actually do some stuff. */ @@ -1081,6 +1118,124 @@ int main(int argc, char **argv) } } + /* + * Swap out the public key for a different one, if asked to. + */ + if (certfile) { + Filename *certfilename = filename_from_str(certfile); + LoadedFile *certfile_lf; + const char *error = NULL; + + if (!strcmp(certfile, "-")) + certfile_lf = lf_load_keyfile_fp(stdin, &error); + else + certfile_lf = lf_load_keyfile(certfilename, &error); + + filename_free(certfilename); + + if (!certfile_lf) { + fprintf(stderr, "puttygen: unable to load certificate file `%s': " + "%s\n", certfile, error); + RETURN(1); + } + + char *algname = NULL; + char *comment = NULL; + strbuf *pub = strbuf_new(); + if (!ppk_loadpub_s(BinarySource_UPCAST(certfile_lf), &algname, + BinarySink_UPCAST(pub), &comment, &error)) { + fprintf(stderr, "puttygen: unable to load certificate file `%s': " + "%s\n", certfile, error); + strbuf_free(pub); + sfree(algname); + sfree(comment); + lf_free(certfile_lf); + RETURN(1); + } + + lf_free(certfile_lf); + sfree(comment); + + const ssh_keyalg *alg = find_pubkey_alg(algname); + if (!alg) { + fprintf(stderr, "puttygen: certificate file `%s' has unsupported " + "algorithm name `%s'\n", certfile, algname); + strbuf_free(pub); + sfree(algname); + RETURN(1); + } + + sfree(algname); + + /* Check the two public keys match apart from certificates */ + strbuf *old_basepub = strbuf_new(); + ssh_key_public_blob(ssh_key_base_key(ssh2key->key), + BinarySink_UPCAST(old_basepub)); + + ssh_key *new_pubkey = ssh_key_new_pub(alg, ptrlen_from_strbuf(pub)); + strbuf *new_basepub = strbuf_new(); + ssh_key_public_blob(ssh_key_base_key(new_pubkey), + BinarySink_UPCAST(new_basepub)); + ssh_key_free(new_pubkey); + + bool match = ptrlen_eq_ptrlen(ptrlen_from_strbuf(old_basepub), + ptrlen_from_strbuf(new_basepub)); + strbuf_free(old_basepub); + strbuf_free(new_basepub); + + if (!match) { + fprintf(stderr, "puttygen: certificate in `%s' does not match " + "public key in `%s'\n", certfile, infile); + strbuf_free(pub); + RETURN(1); + } + + strbuf *priv = strbuf_new_nm(); + ssh_key_private_blob(ssh2key->key, BinarySink_UPCAST(priv)); + ssh_key *newkey = ssh_key_new_priv( + alg, ptrlen_from_strbuf(pub), ptrlen_from_strbuf(priv)); + strbuf_free(pub); + strbuf_free(priv); + + if (!newkey) { + fprintf(stderr, "puttygen: unable to combine certificate in `%s' " + "with private key\n", certfile); + RETURN(1); + } + + ssh_key_free(ssh2key->key); + ssh2key->key = newkey; + } else if (remove_cert) { + /* + * Removing a certificate can be meaningfully done to a pure + * public key blob, as well as a full key pair. + */ + if (ssh2key) { + ssh_key *newkey = ssh_key_clone(ssh_key_base_key(ssh2key->key)); + ssh_key_free(ssh2key->key); + ssh2key->key = newkey; + } else if (ssh2blob) { + ptrlen algname = pubkey_blob_to_alg_name( + ptrlen_from_strbuf(ssh2blob)); + + const ssh_keyalg *alg = find_pubkey_alg_len(algname); + + if (!alg) { + fprintf(stderr, "puttygen: input file `%s' has unsupported " + "algorithm name `%.*s'\n", infile, + PTRLEN_PRINTF(algname)); + RETURN(1); + } + + ssh_key *tmpkey = ssh_key_new_pub( + alg, ptrlen_from_strbuf(ssh2blob)); + strbuf_clear(ssh2blob); + ssh_key_public_blob(ssh_key_base_key(tmpkey), + BinarySink_UPCAST(ssh2blob)); + ssh_key_free(tmpkey); + } + } + /* * Unless we're changing the passphrase, the old one (if any) is a * reasonable default. diff --git a/windows/puttygen.c b/windows/puttygen.c index 281d17c0..5beaed12 100644 --- a/windows/puttygen.c +++ b/windows/puttygen.c @@ -758,7 +758,8 @@ enum { IDC_GIVEHELP, IDC_IMPORT, IDC_EXPORT_OPENSSH_AUTO, IDC_EXPORT_OPENSSH_NEW, - IDC_EXPORT_SSHCOM + IDC_EXPORT_SSHCOM, + IDC_ADDCERT, IDC_REMCERT, }; static const int nokey_ids[] = { IDC_NOKEY, 0 }; @@ -812,6 +813,8 @@ void ui_set_state(HWND hwnd, struct MainDlgState *state, int status) MF_GRAYED|MF_BYCOMMAND); EnableMenuItem(state->cvtmenu, IDC_EXPORT_SSHCOM, MF_GRAYED|MF_BYCOMMAND); + EnableMenuItem(state->keymenu, IDC_ADDCERT, MF_GRAYED|MF_BYCOMMAND); + EnableMenuItem(state->keymenu, IDC_REMCERT, MF_GRAYED|MF_BYCOMMAND); break; case 1: /* generating key */ hidemany(hwnd, nokey_ids, true); @@ -845,6 +848,8 @@ void ui_set_state(HWND hwnd, struct MainDlgState *state, int status) MF_GRAYED|MF_BYCOMMAND); EnableMenuItem(state->cvtmenu, IDC_EXPORT_SSHCOM, MF_GRAYED|MF_BYCOMMAND); + EnableMenuItem(state->keymenu, IDC_ADDCERT, MF_GRAYED|MF_BYCOMMAND); + EnableMenuItem(state->keymenu, IDC_REMCERT, MF_GRAYED|MF_BYCOMMAND); break; case 2: hidemany(hwnd, nokey_ids, true); @@ -884,6 +889,35 @@ void ui_set_state(HWND hwnd, struct MainDlgState *state, int status) do_export_menuitem(IDC_EXPORT_OPENSSH_NEW, SSH_KEYTYPE_OPENSSH_NEW); do_export_menuitem(IDC_EXPORT_SSHCOM, SSH_KEYTYPE_SSHCOM); #undef do_export_menuitem + /* + * Enable certificate menu items similarly. + */ + { + bool add_cert_allowed = false, rem_cert_allowed = false; + + if (state->ssh2 && state->ssh2key.key) { + const ssh_keyalg *alg = ssh_key_alg(state->ssh2key.key); + if (alg->is_certificate) { + /* If there's a certificate, we can remove it */ + rem_cert_allowed = true; + /* And reset to the base algorithm for the next check */ + alg = alg->base_alg; + } + + /* Now, do we have any certified version of this alg? */ + for (size_t i = 0; i < n_keyalgs; i++) { + if (all_keyalgs[i]->base_alg == alg) { + add_cert_allowed = true; + break; + } + } + } + + EnableMenuItem(state->keymenu, IDC_ADDCERT, MF_BYCOMMAND | + (add_cert_allowed ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(state->keymenu, IDC_REMCERT, MF_BYCOMMAND | + (rem_cert_allowed ? MF_ENABLED : MF_GRAYED)); + } break; } } @@ -994,6 +1028,23 @@ void ui_set_fptype(HWND hwnd, struct MainDlgState *state, int option) } } +static void update_ui_after_ssh2_pubkey_change( + HWND hwnd, struct MainDlgState *state) +{ + /* Smaller version of update_ui_after_load which doesn't need to + * be told things like the passphrase, which we aren't changing + * anyway */ + char *savecomment = state->ssh2key.comment; + state->ssh2key.comment = NULL; + char *fp = ssh2_fingerprint(state->ssh2key.key, state->fptype); + state->ssh2key.comment = savecomment; + + SetDlgItemText(hwnd, IDC_FINGERPRINT, fp); + sfree(fp); + + setupbigedit2(hwnd, IDC_KEYDISPLAY, IDC_PKSTATIC, &state->ssh2key); +} + static void update_ui_after_load(HWND hwnd, struct MainDlgState *state, const char *passphrase, int type, RSAKey *newkey1, ssh2_userkey *newkey2) @@ -1024,24 +1075,12 @@ static void update_ui_after_load(HWND hwnd, struct MainDlgState *state, */ setupbigedit1(hwnd, IDC_KEYDISPLAY, IDC_PKSTATIC, &state->key); } else { - char *fp; - char *savecomment; - state->ssh2 = true; state->commentptr = &state->ssh2key.comment; state->ssh2key = *newkey2; /* structure copy */ sfree(newkey2); - savecomment = state->ssh2key.comment; - state->ssh2key.comment = NULL; - fp = ssh2_fingerprint(state->ssh2key.key, state->fptype); - state->ssh2key.comment = savecomment; - - SetDlgItemText(hwnd, IDC_FINGERPRINT, fp); - sfree(fp); - - setupbigedit2(hwnd, IDC_KEYDISPLAY, - IDC_PKSTATIC, &state->ssh2key); + update_ui_after_ssh2_pubkey_change(hwnd, state); } SetDlgItemText(hwnd, IDC_COMMENTEDIT, *state->commentptr); @@ -1165,6 +1204,105 @@ void load_key_file(HWND hwnd, struct MainDlgState *state, burnstr(passphrase); } +void add_certificate(HWND hwnd, struct MainDlgState *state, + Filename *filename) +{ + int type = key_type(filename); + if (type != SSH_KEYTYPE_SSH2_PUBLIC_RFC4716 && + type != SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH) { + char *msg = dupprintf("Couldn't load certificate (%s)", + key_type_to_str(type)); + message_box(hwnd, msg, "PuTTYgen Error", MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + sfree(msg); + return; + } + + char *algname = NULL; + char *comment = NULL; + const char *error = NULL; + strbuf *pub = strbuf_new(); + if (!ppk_loadpub_f(filename, &algname, BinarySink_UPCAST(pub), &comment, + &error)) { + char *msg = dupprintf("Couldn't load certificate (%s)", error); + message_box(hwnd, msg, "PuTTYgen Error", MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + sfree(msg); + return; + } + + sfree(comment); + + const ssh_keyalg *alg = find_pubkey_alg(algname); + if (!alg) { + char *msg = dupprintf("Couldn't load certificate (unsupported " + "algorithm name '%s')", algname); + message_box(hwnd, msg, "PuTTYgen Error", MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + sfree(msg); + sfree(algname); + strbuf_free(pub); + return; + } + + sfree(algname); + + /* Check the two public keys match apart from certificates */ + strbuf *old_basepub = strbuf_new(); + ssh_key_public_blob(ssh_key_base_key(state->ssh2key.key), + BinarySink_UPCAST(old_basepub)); + + ssh_key *new_pubkey = ssh_key_new_pub(alg, ptrlen_from_strbuf(pub)); + strbuf *new_basepub = strbuf_new(); + ssh_key_public_blob(ssh_key_base_key(new_pubkey), + BinarySink_UPCAST(new_basepub)); + ssh_key_free(new_pubkey); + + bool match = ptrlen_eq_ptrlen(ptrlen_from_strbuf(old_basepub), + ptrlen_from_strbuf(new_basepub)); + strbuf_free(old_basepub); + strbuf_free(new_basepub); + + if (!match) { + char *msg = dupprintf("Certificate is for a different public key"); + message_box(hwnd, msg, "PuTTYgen Error", MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + sfree(msg); + strbuf_free(pub); + return; + } + + strbuf *priv = strbuf_new_nm(); + ssh_key_private_blob(state->ssh2key.key, BinarySink_UPCAST(priv)); + ssh_key *newkey = ssh_key_new_priv( + alg, ptrlen_from_strbuf(pub), ptrlen_from_strbuf(priv)); + strbuf_free(pub); + strbuf_free(priv); + + if (!newkey) { + char *msg = dupprintf("Couldn't combine certificate with key"); + message_box(hwnd, msg, "PuTTYgen Error", MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + sfree(msg); + return; + } + + ssh_key_free(state->ssh2key.key); + state->ssh2key.key = newkey; + + update_ui_after_ssh2_pubkey_change(hwnd, state); + ui_set_state(hwnd, state, 2); +} + +void remove_certificate(HWND hwnd, struct MainDlgState *state) +{ + ssh_key *newkey = ssh_key_clone(ssh_key_base_key(state->ssh2key.key)); + ssh_key_free(state->ssh2key.key); + state->ssh2key.key = newkey; + update_ui_after_ssh2_pubkey_change(hwnd, state); + ui_set_state(hwnd, state, 2); +} + static void start_generating_key(HWND hwnd, struct MainDlgState *state) { static const char generating_msg[] = @@ -1250,6 +1388,11 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, menu1 = CreateMenu(); AppendMenu(menu1, MF_ENABLED, IDC_GENERATE, "&Generate key pair"); AppendMenu(menu1, MF_SEPARATOR, 0, 0); + AppendMenu(menu1, MF_ENABLED, IDC_ADDCERT, + "Add &certificate to key"); + AppendMenu(menu1, MF_ENABLED, IDC_REMCERT, + "Remove certificate from key"); + AppendMenu(menu1, MF_SEPARATOR, 0, 0); AppendMenu(menu1, MF_ENABLED, IDC_KEYSSH1, "SSH-&1 key (RSA)"); AppendMenu(menu1, MF_ENABLED, IDC_KEYSSH2RSA, "SSH-2 &RSA key"); AppendMenu(menu1, MF_ENABLED, IDC_KEYSSH2DSA, "SSH-2 &DSA key"); @@ -1870,6 +2013,30 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, } } break; + case IDC_ADDCERT: + if (HIWORD(wParam) != BN_CLICKED) + break; + state = + (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); + if (state->key_exists && !state->generation_thread_exists) { + char filename[FILENAME_MAX]; + if (prompt_keyfile(hwnd, "Load certificate:", filename, false, + false)) { + Filename *fn = filename_from_str(filename); + add_certificate(hwnd, state, fn); + filename_free(fn); + } + } + break; + case IDC_REMCERT: + if (HIWORD(wParam) != BN_CLICKED) + break; + state = + (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); + if (state->key_exists && !state->generation_thread_exists) { + remove_certificate(hwnd, state); + } + break; } return 0; case WM_DONEKEY: