From 6737a19072f39942a67708fe7353a38af35a787a Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 30 Jul 2022 16:10:45 +0100 Subject: [PATCH] cmdgen: human-readable certificate info dump. The recently added SeatDialogText type was just what I needed to add a method to the ssh_key vtable for dumping certificate information in a human-readable format. It will be good for displaying in a Windows dialog box as well as in cmdgen's text format. This commit introduces and implements the new method, and adds a --cert-info mode to command-line Unix PuTTYgen that uses it. The Windows side will follow shortly. --- cmdgen.c | 85 ++++++++++++++++- crypto/openssh-certs.c | 202 +++++++++++++++++++++++++++++++++++++++++ ssh.h | 3 + 3 files changed, 289 insertions(+), 1 deletion(-) diff --git a/cmdgen.c b/cmdgen.c index 044d86aa..963464ea 100644 --- a/cmdgen.c +++ b/cmdgen.c @@ -237,7 +237,7 @@ int main(int argc, char **argv) enum { NOKEYGEN, RSA1, RSA2, DSA, ECDSA, EDDSA } keytype = NOKEYGEN; char *outfile = NULL, *outfiletmp = NULL; enum { PRIVATE, PUBLIC, PUBLICO, FP, OPENSSH_AUTO, - OPENSSH_NEW, SSHCOM, TEXT } outtype = PRIVATE; + OPENSSH_NEW, SSHCOM, TEXT, CERTINFO } outtype = PRIVATE; int bits = -1; const char *comment = NULL; char *origcomment = NULL; @@ -368,6 +368,10 @@ int main(int argc, char **argv) } } else if (!strcmp(opt, "-dump")) { outtype = TEXT; + } else if (!strcmp(opt, "-cert-info") || + !strcmp(opt, "-certinfo") || + !strcmp(opt, "-cert_info")) { + outtype = CERTINFO; } else if (!strcmp(opt, "-primes")) { if (!val && argc > 1) --argc, val = *++argv; @@ -594,6 +598,8 @@ int main(int argc, char **argv) outtype = SSHCOM, sshver = 2; else if (!strcmp(p, "text")) outtype = TEXT; + else if (!strcmp(p, "cert-info")) + outtype = CERTINFO; else { fprintf(stderr, "puttygen: unknown output type `%s'\n", p); @@ -1524,6 +1530,83 @@ int main(int argc, char **argv) key_components_free(kc); break; } + + case CERTINFO: { + if (sshver == 1) { + fprintf(stderr, "puttygen: SSH-1 keys cannot contain " + "certificates\n"); + RETURN(1); + } + + const ssh_keyalg *alg; + ssh_key *sk; + bool sk_allocated = false; + + if (ssh2key) { + sk = ssh2key->key; + alg = ssh_key_alg(sk); + } else { + assert(ssh2blob); + ptrlen algname = pubkey_blob_to_alg_name( + ptrlen_from_strbuf(ssh2blob)); + alg = find_pubkey_alg_len(algname); + if (!alg) { + fprintf(stderr, "puttygen: cannot extract certificate info " + "from public key of unknown type '%.*s'\n", + PTRLEN_PRINTF(algname)); + RETURN(1); + } + sk = ssh_key_new_pub(alg, ptrlen_from_strbuf(ssh2blob)); + if (!sk) { + fprintf(stderr, "puttygen: unable to decode public key\n"); + RETURN(1); + } + sk_allocated = true; + } + + if (!alg->is_certificate) { + fprintf(stderr, "puttygen: key is not a certificate\n"); + } else { + SeatDialogText *text = ssh_key_cert_info(sk); + + FILE *fp; + if (outfile) { + fp = f_open(outfilename, "w", false); + if (!fp) { + fprintf(stderr, "unable to open output file\n"); + exit(1); + } + } else { + fp = stdout; + } + + for (SeatDialogTextItem *item = text->items, + *end = item+text->nitems; item < end; item++) { + switch (item->type) { + case SDT_MORE_INFO_KEY: + fprintf(fp, "%s", item->text); + break; + case SDT_MORE_INFO_VALUE_SHORT: + fprintf(fp, ": %s\n", item->text); + break; + case SDT_MORE_INFO_VALUE_BLOB: + fprintf(fp, ":\n%s\n", item->text); + break; + default: + break; + } + } + + if (outfile) + fclose(fp); + + seat_dialog_text_free(text); + } + + if (sk_allocated) + ssh_key_free(sk); + break; + } } out: diff --git a/crypto/openssh-certs.c b/crypto/openssh-certs.c index 73218e7c..5e86f928 100644 --- a/crypto/openssh-certs.c +++ b/crypto/openssh-certs.c @@ -3,6 +3,7 @@ */ #include "ssh.h" +#include "putty.h" enum { SSH_CERT_TYPE_USER = 1, @@ -203,6 +204,7 @@ static void opensshcert_private_blob(ssh_key *key, BinarySink *bs); static void opensshcert_openssh_blob(ssh_key *key, BinarySink *bs); static void opensshcert_ca_public_blob(ssh_key *key, BinarySink *bs); static void opensshcert_cert_id_string(ssh_key *key, BinarySink *bs); +static SeatDialogText *opensshcert_cert_info(ssh_key *key); static bool opensshcert_has_private(ssh_key *key); static char *opensshcert_cache_str(ssh_key *key); static key_components *opensshcert_components(ssh_key *key); @@ -263,6 +265,7 @@ static const ssh_keyalg *opensshcert_related_alg(const ssh_keyalg *self, .ca_public_blob = opensshcert_ca_public_blob, \ .check_cert = opensshcert_check_cert, \ .cert_id_string = opensshcert_cert_id_string, \ + .cert_info = opensshcert_cert_info, \ .pubkey_bits = opensshcert_pubkey_bits, \ .supported_flags = opensshcert_supported_flags, \ .alternate_ssh_id = opensshcert_alternate_ssh_id, \ @@ -666,6 +669,205 @@ static key_components *opensshcert_components(ssh_key *key) return kc; } +static SeatDialogText *opensshcert_cert_info(ssh_key *key) +{ + opensshcert_key *ck = container_of(key, opensshcert_key, sshk); + SeatDialogText *text = seat_dialog_text_new(); + strbuf *tmp = strbuf_new(); + + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Certificate type"); + switch (ck->type) { + case SSH_CERT_TYPE_HOST: + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "host key"); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Valid host names"); + break; + case SSH_CERT_TYPE_USER: + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "user authentication key"); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Valid user names"); + break; + default: + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "unknown type %" PRIu32, ck->type); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Valid principals"); + break; + } + + { + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf( + ck->valid_principals)); + const char *sep = ""; + strbuf_clear(tmp); + while (get_avail(src)) { + ptrlen principal = get_string(src); + if (get_err(src)) + break; + put_dataz(tmp, sep); + sep = ","; + put_datapl(tmp, principal); + } + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "%s", tmp->s); + } + + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Validity period"); + strbuf_clear(tmp); + if (ck->valid_after == 0) { + if (ck->valid_before == 0xFFFFFFFFFFFFFFFF) { + put_dataz(tmp, "forever"); + } else { + put_dataz(tmp, "until "); + opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp), + ck->valid_before); + } + } else { + if (ck->valid_before == 0xFFFFFFFFFFFFFFFF) { + put_dataz(tmp, "after "); + opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp), + ck->valid_after); + } else { + opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp), + ck->valid_after); + put_dataz(tmp, " - "); + opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp), + ck->valid_before); + } + } + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", tmp->s); + + /* + * List critical options we know about. (This is everything listed + * in PROTOCOL.certkeys that isn't specific to U2F/FIDO key types + * that PuTTY doesn't currently support.) + */ + { + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf( + ck->critical_options)); + strbuf_clear(tmp); + while (get_avail(src)) { + ptrlen key = get_string(src); + ptrlen value = get_string(src); + if (get_err(src)) + break; + if (ck->type == SSH_CERT_TYPE_USER && + ptrlen_eq_string(key, "source-address")) { + BinarySource src2[1]; + BinarySource_BARE_INIT_PL(src2, value); + ptrlen addresslist = get_string(src2); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Permitted client IP addresses"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "%.*s", PTRLEN_PRINTF(addresslist)); + } else if (ck->type == SSH_CERT_TYPE_USER && + ptrlen_eq_string(key, "force-command")) { + BinarySource src2[1]; + BinarySource_BARE_INIT_PL(src2, value); + ptrlen command = get_string(src2); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Forced remote command"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "%.*s", PTRLEN_PRINTF(command)); + } + } + } + + /* + * List certificate extensions. Again, we go through everything in + * PROTOCOL.certkeys that isn't specific to U2F/FIDO key types. + * But we also flip the sense round for user-readability: I think + * it's more likely that the typical key will permit all these + * things, so we emit no output in that case, and only mention the + * things that _aren't_ enabled. + */ + + bool x11_ok = false, agent_ok = false, portfwd_ok = false; + bool pty_ok = false, user_rc_ok = false; + + { + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf( + ck->extensions)); + while (get_avail(src)) { + ptrlen key = get_string(src); + /* ptrlen value = */ get_string(src); // nothing needs this yet + if (get_err(src)) + break; + if (ptrlen_eq_string(key, "permit-X11-forwarding")) { + x11_ok = true; + } else if (ptrlen_eq_string(key, "permit-agent-forwarding")) { + agent_ok = true; + } else if (ptrlen_eq_string(key, "permit-port-forwarding")) { + portfwd_ok = true; + } else if (ptrlen_eq_string(key, "permit-pty")) { + pty_ok = true; + } else if (ptrlen_eq_string(key, "permit-user-rc")) { + user_rc_ok = true; + } + } + } + + if (ck->type == SSH_CERT_TYPE_USER) { + if (!x11_ok) { + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "X11 forwarding permitted"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no"); + } + if (!agent_ok) { + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Agent forwarding permitted"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no"); + } + if (!portfwd_ok) { + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Port forwarding permitted"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no"); + } + if (!pty_ok) { + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "PTY allocation permitted"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no"); + } + if (!user_rc_ok) { + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Running user ~/.ssh.rc permitted"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no"); + } + } + + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Certificate ID string"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "%s", ck->key_id->s); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Certificate serial number"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, + "%" PRIu64, ck->serial); + + char *fp = ssh2_fingerprint_blob(ptrlen_from_strbuf(ck->signature_key), + SSH_FPTYPE_DEFAULT); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Fingerprint of signing CA key"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", fp); + sfree(fp); + + fp = ssh2_fingerprint(ck->basekey, SSH_FPTYPE_DEFAULT); + seat_dialog_text_append(text, SDT_MORE_INFO_KEY, + "Fingerprint of underlying key"); + seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", fp); + sfree(fp); + + strbuf_free(tmp); + return text; +} + static int opensshcert_pubkey_bits(const ssh_keyalg *self, ptrlen blob) { BinarySource src[1]; diff --git a/ssh.h b/ssh.h index 454c5449..bf4f04d7 100644 --- a/ssh.h +++ b/ssh.h @@ -851,6 +851,7 @@ struct ssh_keyalg { uint64_t time, const ca_options *opts, BinarySink *error); void (*cert_id_string)(ssh_key *key, BinarySink *); + SeatDialogText *(*cert_info)(ssh_key *key); /* 'Class methods' that don't deal with an ssh_key at all */ int (*pubkey_bits) (const ssh_keyalg *self, ptrlen blob); @@ -903,6 +904,8 @@ static inline void ssh_key_ca_public_blob(ssh_key *key, BinarySink *bs) { key->vt->ca_public_blob(key, bs); } static inline void ssh_key_cert_id_string(ssh_key *key, BinarySink *bs) { key->vt->cert_id_string(key, bs); } +static inline SeatDialogText *ssh_key_cert_info(ssh_key *key) +{ return key->vt->cert_info(key); } static inline bool ssh_key_check_cert( ssh_key *key, bool host, ptrlen principal, uint64_t time, const ca_options *opts, BinarySink *error)