1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 17:38:00 +00:00
putty-source/crypto/openssh-certs.c
Simon Tatham bbd46afd91 opensshcert_components: switch expiry times to UTC.
Jacob points out that the output of 'puttygen --dump', where the
key_components are used, is much more likely to need to be machine-
than human-readable, and so it makes more sense to use a date/time
format that's invariant under external changes such as locale.

(He also points out that Windows's time zone description strings are
overly verbose!)
2022-09-12 09:34:01 +01:00

1161 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), time);
goto out;
}
if (time >= ck->valid_before) {
put_fmt(error, "Certificate expired at ");
opensshcert_time_to_iso8601(BinarySink_UPCAST(error), time);
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);
}