/* * Public key type for OpenSSH certificates. */ #include "ssh.h" #include "putty.h" enum { SSH_CERT_TYPE_USER = 1, SSH_CERT_TYPE_HOST = 2, }; typedef struct opensshcert_key { strbuf *nonce; uint64_t serial; uint32_t type; strbuf *key_id; strbuf *valid_principals; uint64_t valid_after, valid_before; strbuf *critical_options; strbuf *extensions; strbuf *reserved; strbuf *signature_key; strbuf *signature; ssh_key *basekey; ssh_key sshk; } opensshcert_key; typedef struct blob_fmt { const unsigned *fmt; size_t len; } blob_fmt; typedef struct opensshcert_extra { /* * OpenSSH certificate formats aren't completely consistent about * the relationship between the public+private blob uploaded to * the agent for the certified key type, and the one for the base * key type. Here we specify the mapping. * * Each of these foo_fmt strings indicates the layout of a * particular version of the key, in the form of an array of * integers together with a length, with each integer describing * one of the components of the key. The integers are defined by * enums, so that they're tightly packed; the general idea is that * if you're converting from one form to another, then you use the * format list for the source format to read out a succession of * SSH strings from the source data and put them in an array * indexed by the integer ids, and then use the list for the * destination format to write the strings out to the destination * in the right (maybe different) order. * * pub_fmt describes the format of the public-key blob for the * base key type, not counting the initial string giving the key * type identifier itself. As far as I know, this always matches * the format of the public-key data appearing in the middle of * the certificate. * * base_ossh_fmt describes the format of the full OpenSSH blob * appearing in the ssh-agent protocol for the base key, * containing the public and private key data. * * cert_ossh_fmt describes the format of the OpenSSH blob for the * certificate key format, beginning just after the certificate * string itself. */ blob_fmt pub_fmt, base_ossh_fmt, cert_ossh_fmt; /* * The RSA-SHA2 algorithm names have their SSH id set to names * like "rsa-sha2-512-cert-...", which is what will be received in * the KEXINIT algorithm list if a host key in one of those * algorithms is presented. But the _key_ type id that will appear * in the public key blob is "ssh-rsa-cert-...". So we need a * separate field to indicate the key type id we expect to see in * certified public keys, and also the one we want to put back * into the artificial public blob we make to pass to the * constructor for the underlying key. * * (In rsa.c this is managed much more simply, because everything * sharing the same vtable wants the same key type id.) */ const char *cert_key_ssh_id, *base_key_ssh_id; } opensshcert_extra; /* * The actual integer arrays defining the per-key blob formats. */ /* DSA is the most orthodox: only the obviously necessary public key * info appears at all, it's in the same order everywhere, and none of * it is repeated unnecessarily */ enum { DSA_p, DSA_q, DSA_g, DSA_y, DSA_x }; static const unsigned dsa_pub_fmt[] = { DSA_p, DSA_q, DSA_g, DSA_y }; static const unsigned dsa_base_ossh_fmt[] = { DSA_p, DSA_q, DSA_g, DSA_y, DSA_x }; static const unsigned dsa_cert_ossh_fmt[] = { DSA_x }; /* ECDSA is almost as nice, except that it pointlessly mentions the * curve name in the public data, which shouldn't be necessary given * that the SSH key id has already implied it. But at least that's * consistent everywhere. */ enum { ECDSA_curve, ECDSA_point, ECDSA_exp }; static const unsigned ecdsa_pub_fmt[] = { ECDSA_curve, ECDSA_point }; static const unsigned ecdsa_base_ossh_fmt[] = { ECDSA_curve, ECDSA_point, ECDSA_exp }; static const unsigned ecdsa_cert_ossh_fmt[] = { ECDSA_exp }; /* Ed25519 has the oddity that the private data following the * certificate in the OpenSSH blob is preceded by an extra copy of the * public data, for no obviously necessary reason since that doesn't * happen in any of the rest of these formats */ enum { EDDSA_point, EDDSA_exp }; static const unsigned eddsa_pub_fmt[] = { EDDSA_point }; static const unsigned eddsa_base_ossh_fmt[] = { EDDSA_point, EDDSA_exp }; static const unsigned eddsa_cert_ossh_fmt[] = { EDDSA_point, EDDSA_exp }; /* And RSA has the quirk that the modulus and exponent are reversed in * the base key type's OpenSSH blob! */ enum { RSA_e, RSA_n, RSA_d, RSA_p, RSA_q, RSA_iqmp }; static const unsigned rsa_pub_fmt[] = { RSA_e, RSA_n }; static const unsigned rsa_base_ossh_fmt[] = { RSA_n, RSA_e, RSA_d, RSA_p, RSA_q, RSA_iqmp }; static const unsigned rsa_cert_ossh_fmt[] = { RSA_d, RSA_p, RSA_q, RSA_iqmp }; /* * Routines to transform one kind of blob into another based on those * foo_fmt integer arrays. */ typedef struct BlobTransformer { ptrlen *parts; size_t nparts; } BlobTransformer; #define BLOBTRANS_DECLARE(bt) BlobTransformer bt[1] = { { NULL, 0 } } static inline void blobtrans_clear(BlobTransformer *bt) { sfree(bt->parts); bt->parts = NULL; bt->nparts = 0; } static inline bool blobtrans_read(BlobTransformer *bt, BinarySource *src, blob_fmt blob) { size_t nparts = bt->nparts; for (size_t i = 0; i < blob.len; i++) if (nparts < blob.fmt[i]+1) nparts = blob.fmt[i]+1; if (nparts > bt->nparts) { bt->parts = sresize(bt->parts, nparts, ptrlen); while (bt->nparts < nparts) bt->parts[bt->nparts++] = make_ptrlen(NULL, 0); } for (size_t i = 0; i < blob.len; i++) { size_t j = blob.fmt[i]; ptrlen part = get_string(src); if (bt->parts[j].ptr) { /* * If the same string appears in both the public blob and * the private data, check they match. (This happens in * Ed25519: an extra copy of the public point string * appears in the certified OpenSSH data after the * certificate and before the private key.) */ if (!ptrlen_eq_ptrlen(bt->parts[j], part)) return false; } bt->parts[j] = part; } return true; } static inline void blobtrans_write(BlobTransformer *bt, BinarySink *bs, blob_fmt blob) { for (size_t i = 0; i < blob.len; i++) { assert(i < bt->nparts); ptrlen part = bt->parts[blob.fmt[i]]; assert(part.ptr); put_stringpl(bs, part); } } /* * Forward declarations. */ static ssh_key *opensshcert_new_pub(const ssh_keyalg *self, ptrlen pub); static ssh_key *opensshcert_new_priv( const ssh_keyalg *self, ptrlen pub, ptrlen priv); static ssh_key *opensshcert_new_priv_openssh( const ssh_keyalg *self, BinarySource *src); static void opensshcert_freekey(ssh_key *key); static char *opensshcert_invalid(ssh_key *key, unsigned flags); static void opensshcert_sign(ssh_key *key, ptrlen data, unsigned flags, BinarySink *bs); static bool opensshcert_verify(ssh_key *key, ptrlen sig, ptrlen data); static void opensshcert_public_blob(ssh_key *key, BinarySink *bs); 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); static ssh_key *opensshcert_base_key(ssh_key *key); static bool opensshcert_check_cert( ssh_key *key, bool host, ptrlen principal, uint64_t time, const ca_options *opts, BinarySink *error); static int opensshcert_pubkey_bits(const ssh_keyalg *self, ptrlen blob); static unsigned opensshcert_supported_flags(const ssh_keyalg *self); static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self, unsigned flags); static char *opensshcert_alg_desc(const ssh_keyalg *self); static bool opensshcert_variable_size(const ssh_keyalg *self); static const ssh_keyalg *opensshcert_related_alg(const ssh_keyalg *self, const ssh_keyalg *base); /* * Top-level vtables for the certified key formats, defined via a list * macro so I can also make an array of them all. */ #define KEYALG_LIST(X) \ X(ssh_dsa, "ssh-dss", "ssh-dss", dsa) \ X(ssh_rsa, "ssh-rsa", "ssh-rsa", rsa) \ X(ssh_rsa_sha256, "rsa-sha2-256", "ssh-rsa", rsa) \ X(ssh_rsa_sha512, "rsa-sha2-512", "ssh-rsa", rsa) \ X(ssh_ecdsa_ed25519, "ssh-ed25519", "ssh-ed25519", eddsa) \ X(ssh_ecdsa_nistp256, "ecdsa-sha2-nistp256","ecdsa-sha2-nistp256", ecdsa) \ X(ssh_ecdsa_nistp384, "ecdsa-sha2-nistp384","ecdsa-sha2-nistp384", ecdsa) \ X(ssh_ecdsa_nistp521, "ecdsa-sha2-nistp521","ecdsa-sha2-nistp521", ecdsa) \ /* end of list */ #define KEYALG_DEF(name, ssh_alg_id_prefix, ssh_key_id_prefix, fmt_prefix) \ static const struct opensshcert_extra opensshcert_##name##_extra = { \ .pub_fmt = { .fmt = fmt_prefix ## _pub_fmt, \ .len = lenof(fmt_prefix ## _pub_fmt) }, \ .base_ossh_fmt = { .fmt = fmt_prefix ## _base_ossh_fmt, \ .len = lenof(fmt_prefix ## _base_ossh_fmt) }, \ .cert_ossh_fmt = { .fmt = fmt_prefix ## _cert_ossh_fmt, \ .len = lenof(fmt_prefix ## _cert_ossh_fmt) }, \ .cert_key_ssh_id = ssh_key_id_prefix "-cert-v01@openssh.com", \ .base_key_ssh_id = ssh_key_id_prefix, \ }; \ \ const ssh_keyalg opensshcert_##name = { \ .new_pub = opensshcert_new_pub, \ .new_priv = opensshcert_new_priv, \ .new_priv_openssh = opensshcert_new_priv_openssh, \ .freekey = opensshcert_freekey, \ .invalid = opensshcert_invalid, \ .sign = opensshcert_sign, \ .verify = opensshcert_verify, \ .public_blob = opensshcert_public_blob, \ .private_blob = opensshcert_private_blob, \ .openssh_blob = opensshcert_openssh_blob, \ .has_private = opensshcert_has_private, \ .cache_str = opensshcert_cache_str, \ .components = opensshcert_components, \ .base_key = opensshcert_base_key, \ .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, \ .alg_desc = opensshcert_alg_desc, \ .variable_size = opensshcert_variable_size, \ .related_alg = opensshcert_related_alg, \ .ssh_id = ssh_alg_id_prefix "-cert-v01@openssh.com", \ .cache_id = "opensshcert-" ssh_key_id_prefix, \ .extra = &opensshcert_##name##_extra, \ .is_certificate = true, \ .base_alg = &name, \ }; KEYALG_LIST(KEYALG_DEF) #undef KEYALG_DEF #define KEYALG_LIST_ENTRY(name, algid, keyid, fmt) &opensshcert_##name, static const ssh_keyalg *const opensshcert_all_keyalgs[] = { KEYALG_LIST(KEYALG_LIST_ENTRY) }; #undef KEYALG_LIST_ENTRY static strbuf *get_base_public_blob(BinarySource *src, const opensshcert_extra *extra) { strbuf *basepub = strbuf_new(); put_stringz(basepub, extra->base_key_ssh_id); /* Make the base public key blob out of the public key * material in the certificate. This invocation of the * blobtrans system doesn't do any format translation, but it * does ensure that the right amount of data is copied so that * src ends up in the right position to read the remaining * certificate fields. */ BLOBTRANS_DECLARE(bt); blobtrans_read(bt, src, extra->pub_fmt); blobtrans_write(bt, BinarySink_UPCAST(basepub), extra->pub_fmt); blobtrans_clear(bt); return basepub; } static opensshcert_key *opensshcert_new_shared( const ssh_keyalg *self, ptrlen blob, strbuf **basepub_out) { const opensshcert_extra *extra = self->extra; BinarySource src[1]; BinarySource_BARE_INIT_PL(src, blob); /* Check the initial key-type string */ if (!ptrlen_eq_string(get_string(src), extra->cert_key_ssh_id)) return NULL; opensshcert_key *ck = snew(opensshcert_key); memset(ck, 0, sizeof(*ck)); ck->sshk.vt = self; ck->nonce = strbuf_dup(get_string(src)); strbuf *basepub = get_base_public_blob(src, extra); ck->serial = get_uint64(src); ck->type = get_uint32(src); ck->key_id = strbuf_dup(get_string(src)); ck->valid_principals = strbuf_dup(get_string(src)); ck->valid_after = get_uint64(src); ck->valid_before = get_uint64(src); ck->critical_options = strbuf_dup(get_string(src)); ck->extensions = strbuf_dup(get_string(src)); ck->reserved = strbuf_dup(get_string(src)); ck->signature_key = strbuf_dup(get_string(src)); ck->signature = strbuf_dup(get_string(src)); if (get_err(src)) { ssh_key_free(&ck->sshk); strbuf_free(basepub); return NULL; } *basepub_out = basepub; return ck; } static ssh_key *opensshcert_new_pub(const ssh_keyalg *self, ptrlen pub) { strbuf *basepub; opensshcert_key *ck = opensshcert_new_shared(self, pub, &basepub); if (!ck) return NULL; ck->basekey = ssh_key_new_pub(self->base_alg, ptrlen_from_strbuf(basepub)); strbuf_free(basepub); if (!ck->basekey) { ssh_key_free(&ck->sshk); return NULL; } return &ck->sshk; } static ssh_key *opensshcert_new_priv( const ssh_keyalg *self, ptrlen pub, ptrlen priv) { strbuf *basepub; opensshcert_key *ck = opensshcert_new_shared(self, pub, &basepub); if (!ck) return NULL; ck->basekey = ssh_key_new_priv(self->base_alg, ptrlen_from_strbuf(basepub), priv); strbuf_free(basepub); if (!ck->basekey) { ssh_key_free(&ck->sshk); return NULL; } return &ck->sshk; } static ssh_key *opensshcert_new_priv_openssh( const ssh_keyalg *self, BinarySource *src) { const opensshcert_extra *extra = self->extra; ptrlen cert = get_string(src); strbuf *basepub; opensshcert_key *ck = opensshcert_new_shared(self, cert, &basepub); if (!ck) return NULL; strbuf *baseossh = strbuf_new(); /* Make the base OpenSSH key blob out of the public key blob * returned from opensshcert_new_shared, and the trailing * private data following the certificate */ BLOBTRANS_DECLARE(bt); BinarySource pubsrc[1]; BinarySource_BARE_INIT_PL(pubsrc, ptrlen_from_strbuf(basepub)); get_string(pubsrc); /* skip key type id */ /* blobtrans_read might fail in this case, because we're reading * from two sources and they might fail to match */ bool success = blobtrans_read(bt, pubsrc, extra->pub_fmt) && blobtrans_read(bt, src, extra->cert_ossh_fmt); blobtrans_write(bt, BinarySink_UPCAST(baseossh), extra->base_ossh_fmt); blobtrans_clear(bt); if (!success) { ssh_key_free(&ck->sshk); strbuf_free(basepub); strbuf_free(baseossh); return NULL; } strbuf_free(basepub); BinarySource osshsrc[1]; BinarySource_BARE_INIT_PL(osshsrc, ptrlen_from_strbuf(baseossh)); ck->basekey = ssh_key_new_priv_openssh(self->base_alg, osshsrc); strbuf_free(baseossh); if (!ck->basekey) { ssh_key_free(&ck->sshk); return NULL; } return &ck->sshk; } static void opensshcert_freekey(ssh_key *key) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); /* If this function is called from one of the above constructors * because it failed part way through, we might not have managed * to construct ck->basekey, so it might be NULL. */ if (ck->basekey) ssh_key_free(ck->basekey); strbuf_free(ck->nonce); strbuf_free(ck->key_id); strbuf_free(ck->valid_principals); strbuf_free(ck->critical_options); strbuf_free(ck->extensions); strbuf_free(ck->reserved); strbuf_free(ck->signature_key); strbuf_free(ck->signature); sfree(ck); } static ssh_key *opensshcert_base_key(ssh_key *key) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); return ck->basekey; } /* * Make a public key object from the CA public blob, potentially * taking into account that the signature might override the algorithm * name */ static ssh_key *opensshcert_ca_pub_key( opensshcert_key *ck, ptrlen sig, ptrlen *algname) { ptrlen ca_keyblob = ptrlen_from_strbuf(ck->signature_key); ptrlen alg_source = sig.ptr ? sig : ca_keyblob; if (algname) *algname = pubkey_blob_to_alg_name(alg_source); const ssh_keyalg *ca_alg = pubkey_blob_to_alg(alg_source); if (!ca_alg) return NULL; /* don't even recognise the certifying key type */ return ssh_key_new_pub(ca_alg, ca_keyblob); } static void opensshcert_signature_preimage(opensshcert_key *ck, BinarySink *bs) { const opensshcert_extra *extra = ck->sshk.vt->extra; put_stringz(bs, extra->cert_key_ssh_id); put_stringpl(bs, ptrlen_from_strbuf(ck->nonce)); strbuf *basepub = strbuf_new(); ssh_key_public_blob(ck->basekey, BinarySink_UPCAST(basepub)); BinarySource src[1]; BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(basepub)); get_string(src); /* skip initial key type string */ put_data(bs, get_ptr(src), get_avail(src)); strbuf_free(basepub); put_uint64(bs, ck->serial); put_uint32(bs, ck->type); put_stringpl(bs, ptrlen_from_strbuf(ck->key_id)); put_stringpl(bs, ptrlen_from_strbuf(ck->valid_principals)); put_uint64(bs, ck->valid_after); put_uint64(bs, ck->valid_before); put_stringpl(bs, ptrlen_from_strbuf(ck->critical_options)); put_stringpl(bs, ptrlen_from_strbuf(ck->extensions)); put_stringpl(bs, ptrlen_from_strbuf(ck->reserved)); put_stringpl(bs, ptrlen_from_strbuf(ck->signature_key)); } static void opensshcert_public_blob(ssh_key *key, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); opensshcert_signature_preimage(ck, bs); put_stringpl(bs, ptrlen_from_strbuf(ck->signature)); } static void opensshcert_private_blob(ssh_key *key, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); ssh_key_private_blob(ck->basekey, bs); } static void opensshcert_openssh_blob(ssh_key *key, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); const opensshcert_extra *extra = key->vt->extra; strbuf *cert = strbuf_new(); ssh_key_public_blob(key, BinarySink_UPCAST(cert)); put_stringsb(bs, cert); strbuf *baseossh = strbuf_new_nm(); ssh_key_openssh_blob(ck->basekey, BinarySink_UPCAST(baseossh)); BinarySource basesrc[1]; BinarySource_BARE_INIT_PL(basesrc, ptrlen_from_strbuf(baseossh)); BLOBTRANS_DECLARE(bt); blobtrans_read(bt, basesrc, extra->base_ossh_fmt); blobtrans_write(bt, bs, extra->cert_ossh_fmt); blobtrans_clear(bt); strbuf_free(baseossh); } static void opensshcert_ca_public_blob(ssh_key *key, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); put_datapl(bs, ptrlen_from_strbuf(ck->signature_key)); } static void opensshcert_cert_id_string(ssh_key *key, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); put_datapl(bs, ptrlen_from_strbuf(ck->key_id)); } static bool opensshcert_has_private(ssh_key *key) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); return ssh_key_has_private(ck->basekey); } static char *opensshcert_cache_str(ssh_key *key) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); return ssh_key_cache_str(ck->basekey); } static void opensshcert_time_to_iso8601(BinarySink *bs, uint64_t time) { time_t t = time; char buf[256]; put_data(bs, buf, strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S UTC", gmtime(&t))); } static void opensshcert_string_list_key_components( key_components *kc, strbuf *input, const char *title, const char *title2) { BinarySource src[1]; BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(input)); const char *titles[2] = { title, title2 }; size_t ntitles = (title2 ? 2 : 1); unsigned index = 0; while (get_avail(src)) { for (size_t ti = 0; ti < ntitles; ti++) { ptrlen value = get_string(src); if (get_err(src)) break; char *name = dupprintf("%s_%u", titles[ti], index); key_components_add_text_pl(kc, name, value); sfree(name); } index++; } } static key_components *opensshcert_components(ssh_key *key) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); key_components *kc = ssh_key_components(ck->basekey); key_components_add_binary(kc, "cert_nonce", ptrlen_from_strbuf(ck->nonce)); key_components_add_uint(kc, "cert_serial", ck->serial); switch (ck->type) { case SSH_CERT_TYPE_HOST: key_components_add_text(kc, "cert_type", "host"); break; case SSH_CERT_TYPE_USER: key_components_add_text(kc, "cert_type", "user"); break; default: key_components_add_uint(kc, "cert_type", ck->type); break; } key_components_add_text(kc, "cert_key_id", ck->key_id->s); opensshcert_string_list_key_components(kc, ck->valid_principals, "cert_valid_principal", NULL); key_components_add_uint(kc, "cert_valid_after", ck->valid_after); key_components_add_uint(kc, "cert_valid_before", ck->valid_before); /* Translate the validity period into human-legible dates, but * only if they're not the min/max integer. Rationale: if you see * "584554051223-11-09 07:00:15 UTC" as the expiry time you'll be * as likely to think it's a weird buffer overflow as half a * trillion years in the future! */ if (ck->valid_after != 0) { strbuf *date = strbuf_new(); opensshcert_time_to_iso8601(BinarySink_UPCAST(date), ck->valid_after); key_components_add_text_pl(kc, "cert_valid_after_date", ptrlen_from_strbuf(date)); strbuf_free(date); } if (ck->valid_before != 0xFFFFFFFFFFFFFFFF) { strbuf *date = strbuf_new(); opensshcert_time_to_iso8601(BinarySink_UPCAST(date), ck->valid_before); key_components_add_text_pl(kc, "cert_valid_before_date", ptrlen_from_strbuf(date)); strbuf_free(date); } opensshcert_string_list_key_components(kc, ck->critical_options, "cert_critical_option", "cert_critical_option_data"); opensshcert_string_list_key_components(kc, ck->extensions, "cert_extension", "cert_extension_data"); key_components_add_binary(kc, "cert_ca_key", ptrlen_from_strbuf( ck->signature_key)); ptrlen ca_algname; ssh_key *ca_key = opensshcert_ca_pub_key(ck, make_ptrlen(NULL, 0), &ca_algname); key_components_add_text_pl(kc, "cert_ca_key_algorithm_id", ca_algname); if (ca_key) { key_components *kc_ca_key = ssh_key_components(ca_key); for (size_t i = 0; i < kc_ca_key->ncomponents; i++) { key_component *comp = &kc_ca_key->components[i]; char *subname = dupcat("cert_ca_key_", comp->name); key_components_add_copy(kc, subname, comp); sfree(subname); } key_components_free(kc_ca_key); ssh_key_free(ca_key); } key_components_add_binary(kc, "cert_ca_sig", ptrlen_from_strbuf( ck->signature)); 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(key, ssh_fptype_to_cert(SSH_FPTYPE_DEFAULT)); seat_dialog_text_append(text, SDT_MORE_INFO_KEY, "Fingerprint including certificate"); 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]; BinarySource_BARE_INIT_PL(src, blob); get_string(src); /* key type */ get_string(src); /* nonce */ strbuf *basepub = get_base_public_blob(src, self->extra); int bits = ssh_key_public_bits( self->base_alg, ptrlen_from_strbuf(basepub)); strbuf_free(basepub); return bits; } static unsigned opensshcert_supported_flags(const ssh_keyalg *self) { return ssh_keyalg_supported_flags(self->base_alg); } static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self, unsigned flags) { const char *base_id = ssh_keyalg_alternate_ssh_id(self->base_alg, flags); for (size_t i = 0; i < lenof(opensshcert_all_keyalgs); i++) { const ssh_keyalg *alg_i = opensshcert_all_keyalgs[i]; if (!strcmp(base_id, alg_i->base_alg->ssh_id)) return alg_i->ssh_id; } return self->ssh_id; } static char *opensshcert_alg_desc(const ssh_keyalg *self) { char *base_desc = ssh_keyalg_desc(self->base_alg); char *our_desc = dupcat(base_desc, " cert"); sfree(base_desc); return our_desc; } static bool opensshcert_variable_size(const ssh_keyalg *self) { return ssh_keyalg_variable_size(self->base_alg); } static const ssh_keyalg *opensshcert_related_alg(const ssh_keyalg *self, const ssh_keyalg *base) { for (size_t i = 0; i < lenof(opensshcert_all_keyalgs); i++) { const ssh_keyalg *alg_i = opensshcert_all_keyalgs[i]; if (base == alg_i->base_alg) return alg_i; } return self; } static char *opensshcert_invalid(ssh_key *key, unsigned flags) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); return ssh_key_invalid(ck->basekey, flags); } static bool opensshcert_check_cert( ssh_key *key, bool host, ptrlen principal, uint64_t time, const ca_options *opts, BinarySink *error) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); bool result = false; ssh_key *ca_key = NULL; strbuf *preimage = strbuf_new(); BinarySource src[1]; ptrlen signature = ptrlen_from_strbuf(ck->signature); /* * The OpenSSH certificate spec is one-layer only: it explicitly * forbids using a certified key in turn as the CA. * * If it did not, then we'd also have to recursively verify * everything up the CA chain until we reached the ultimate root, * and then make sure _that_ was something we trusted. (Not to * mention that there'd probably be an additional SSH_CERT_TYPE_CA * or some such, and certificate options saying what kinds of * certificate a CA was trusted to sign for, and ...) */ ca_key = opensshcert_ca_pub_key(ck, make_ptrlen(NULL, 0), NULL); if (!ca_key) { put_fmt(error, "Certificate's signing key is invalid"); goto out; } if (ssh_key_alg(ca_key)->is_certificate) { put_fmt(error, "Certificate is signed with a certified key " "(forbidden by OpenSSH certificate specification)"); goto out; } /* * Now re-instantiate the key in a way that matches the signature * (i.e. so that if the key is an RSA one we get the right subtype * of RSA). */ ssh_key_free(ca_key); ca_key = opensshcert_ca_pub_key(ck, signature, NULL); if (!ca_key) { put_fmt(error, "Certificate's signing key does not match " "signature type"); goto out; } /* Check which signature algorithm is actually in use, because * that might be a reason to reject the certificate (e.g. ssh-rsa * when we wanted rsa-sha2-*). */ const ssh_keyalg *sig_alg = ssh_key_alg(ca_key); if ((sig_alg == &ssh_rsa && !opts->permit_rsa_sha1) || (sig_alg == &ssh_rsa_sha256 && !opts->permit_rsa_sha256) || (sig_alg == &ssh_rsa_sha512 && !opts->permit_rsa_sha512)) { put_fmt(error, "Certificate signature uses '%s' signature type " "(forbidden by user configuration)", sig_alg->ssh_id); goto out; } opensshcert_signature_preimage(ck, BinarySink_UPCAST(preimage)); if (!ssh_key_verify(ca_key, signature, ptrlen_from_strbuf(preimage))) { put_fmt(error, "Certificate's signature is invalid"); goto out; } uint32_t expected_type = host ? SSH_CERT_TYPE_HOST : SSH_CERT_TYPE_USER; if (ck->type != expected_type) { put_fmt(error, "Certificate type is "); switch (ck->type) { case SSH_CERT_TYPE_HOST: put_fmt(error, "host"); break; case SSH_CERT_TYPE_USER: put_fmt(error, "user"); break; default: put_fmt(error, "unknown value %" PRIu32, ck->type); break; } put_fmt(error, "; expected %s", host ? "host" : "user"); goto out; } /* * Check the time bounds on the certificate. */ if (time < ck->valid_after) { put_fmt(error, "Certificate is not valid until "); opensshcert_time_to_iso8601(BinarySink_UPCAST(error), ck->valid_after); goto out; } if (time >= ck->valid_before) { put_fmt(error, "Certificate expired at "); opensshcert_time_to_iso8601(BinarySink_UPCAST(error), ck->valid_before); goto out; } /* * Check that this certificate is for the right thing. * * If valid_principals is a zero-length string then this is * specified to be a carte-blanche certificate valid for any * principal (at least, provided you trust the CA that issued it). */ if (ck->valid_principals->len != 0) { BinarySource_BARE_INIT_PL( src, ptrlen_from_strbuf(ck->valid_principals)); while (get_avail(src)) { ptrlen valid_principal = get_string(src); if (get_err(src)) { put_fmt(error, "Certificate's valid principals list is " "incorrectly formatted"); goto out; } if (ptrlen_eq_ptrlen(valid_principal, principal)) goto principal_ok; } /* * No valid principal matched. Now go through the list a * second time writing the cert contents into the error * message, so that the user can see at a glance what went * wrong. * * (If you've typed the wrong spelling of the host name, you * really need to see "This cert is for 'foo.example.com' and * I was trying to match it against 'foo'", rather than just * "Computer says no".) */ put_fmt(error, "Certificate's %s list [", host ? "hostname" : "username"); BinarySource_BARE_INIT_PL( src, ptrlen_from_strbuf(ck->valid_principals)); const char *sep = ""; while (get_avail(src)) { ptrlen valid_principal = get_string(src); put_fmt(error, "%s\"", sep); put_c_string_literal(error, valid_principal); put_fmt(error, "\""); sep = ", "; } put_fmt(error, "] does not contain expected %s \"", host ? "hostname" : "username"); put_c_string_literal(error, principal); put_fmt(error, "\""); goto out; principal_ok:; } /* * Check for critical options. */ { BinarySource_BARE_INIT_PL( src, ptrlen_from_strbuf(ck->critical_options)); while (get_avail(src)) { ptrlen option = get_string(src); ptrlen data = get_string(src); if (get_err(src)) { put_fmt(error, "Certificate's critical options list is " "incorrectly formatted"); goto out; } /* * If we ever do support any options, this will be where * we insert code to recognise and validate them. * * At present, we implement no critical options at all. * (For host certs, as of 2022-04-20, OpenSSH hasn't * defined any. For user certs, the only SSH server using * this is Uppity, which doesn't support key restrictions * in general.) */ (void)data; /* no options supported => no use made of the data */ /* * Report an unrecognised literal. */ put_fmt(error, "Certificate specifies an unsupported critical " "option \""); put_c_string_literal(error, option); put_fmt(error, "\""); goto out; } } /* If we get here without failing any check, accept the certificate! */ result = true; out: if (ca_key) ssh_key_free(ca_key); strbuf_free(preimage); return result; } static bool opensshcert_verify(ssh_key *key, ptrlen sig, ptrlen data) { /* This method is pure *signature* verification; checking the * certificate is done elsewhere. */ opensshcert_key *ck = container_of(key, opensshcert_key, sshk); return ssh_key_verify(ck->basekey, sig, data); } static void opensshcert_sign(ssh_key *key, ptrlen data, unsigned flags, BinarySink *bs) { opensshcert_key *ck = container_of(key, opensshcert_key, sshk); ssh_key_sign(ck->basekey, data, flags, bs); }