1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 01:18:00 +00:00
putty-source/crypto/openssh-certs.c
Jacob Nevins b760a2a040 Use correct date in cert check error.
When a host certificate was used outside its valid date range, we were
displaying the current time where we meant to show the relevant bound of
the validity range.

(cherry picked from commit 68db3d195d)
2023-04-19 14:19:34 +01:00

1163 lines
43 KiB
C

/*
* 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);
}