1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 17:38:00 +00:00

PuTTYgen: options to add and remove certificates.

This allows you to actually use an OpenSSH user certificate for
authentication, by combining the PPK you already had with the
certificate from the CA to produce a new PPK whose public half
contains the certificate.

I don't intend that this should be the only way to do it. It's
cumbersome to have to use the key passphrase in order to re-encrypt
the modified PPK. But the flip side is that once you've done it you
have everything you need in one convenient file, and also, the
certificate itself is covered by the PPK's tamperproofing (in case you
can think of any attacks in which the admin of your file server swaps
out just the certificate for a different one on the same key). So this
is *a* useful way to do it, even if not the only useful way.

The new options to add and remove certificates are supported by both
Windows GUI PuTTYgen and cmdgen. cmdgen can also operate on pure
public keys, so you can say 'puttygen --remove-certificate
foo-cert.pub' and get back the underlying foo.pub; Windows PuTTYgen
doesn't support that mode, but only because it doesn't in general have
any support for the loaded key not being a full public+private pair.
This commit is contained in:
Simon Tatham 2022-04-19 17:59:46 +01:00
parent 4cde00efc0
commit 7cb3142a57
2 changed files with 337 additions and 15 deletions

157
cmdgen.c
View File

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

View File

@ -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: