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

Certificate-specific ssh_key method suite.

Certificate keys don't work the same as normal keys, so the rest of
the code is going to have to pay attention to whether a key is a
certificate, and if so, treat it differently and do cert-specific
stuff to it. So here's a collection of methods for that purpose.

With one exception, these methods of ssh_key are not expected to be
implemented at all in non-certificate key types: they should only ever
be called once you already know you're dealing with a certificate. So
most of the new method pointers can be left out of the ssh_keyalg
initialisers.

The exception is the base_key method, which retrieves the base key of
a certificate - the underlying one with the certificate stripped off.
It's convenient for non-certificate keys to implement this too, and
just return a pointer to themselves. So I've added an implementation
in nullkey.c doing that. (The returned pointer doesn't transfer
ownership; you have to use the new ssh_key_clone() if you want to keep
the base key after freeing the certificate key.)

The methods _only_ implemented in certificates:

Query methods to return the public key of the CA (for looking up in a
list of trusted ones), and to return the key id string (which exists
to be written into log files).

Obviously, we need a check_cert() method which will verify the CA's
actual signature, not to mention checking all the other details like
the principal and the validity period.

And there's another fiddly method for dealing with the RSA upgrade
system, called 'related_alg'. This is quite like alternate_ssh_id, in
that its job is to upgrade one key algorithm to a related one with
more modern RSA signing flags (or any other similar thing that might
later reuse the same mechanism). But where alternate_ssh_id took the
actual signing flags as an argument, this takes a pointer to the
upgraded base algorithm. So it answers the question "What is to this
key algorithm as you are to its base?" - if you call it on
opensshcert_ssh_rsa and give it ssh_rsa_sha512, it'll give you back
opensshcert_ssh_rsa_sha512.

(It's awkward to have to have another of these fiddly methods, and in
the longer term I'd like to try to clean up their proliferation a bit.
But I even more dislike the alternative of just going through
all_keyalgs looking for a cert algorithm with, say, ssh_rsa_sha512 as
the base: that approach would work fine now but it would be a lurking
time bomb for when all the -cert-v02@ methods appear one day. This
way, each certificate type can upgrade itself to the appropriately
related version. And at least related_alg is only needed if you _are_
a certificate key type - it's not adding yet another piece of
null-method boilerplate to the rest.)
This commit is contained in:
Simon Tatham 2022-04-20 13:06:08 +01:00
parent 34d01e1b65
commit 9f583c4fa8
8 changed files with 294 additions and 4 deletions

View File

@ -504,6 +504,7 @@ const ssh_keyalg ssh_dsa = {
.has_private = dsa_has_private,
.cache_str = dsa_cache_str,
.components = dsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = dsa_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,

View File

@ -1269,6 +1269,7 @@ const ssh_keyalg ssh_ecdsa_ed25519 = {
.has_private = eddsa_has_private,
.cache_str = eddsa_cache_str,
.components = eddsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = ec_shared_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,
@ -1295,6 +1296,7 @@ const ssh_keyalg ssh_ecdsa_ed448 = {
.has_private = eddsa_has_private,
.cache_str = eddsa_cache_str,
.components = eddsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = ec_shared_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,
@ -1325,6 +1327,7 @@ const ssh_keyalg ssh_ecdsa_nistp256 = {
.has_private = ecdsa_has_private,
.cache_str = ecdsa_cache_str,
.components = ecdsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = ec_shared_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,
@ -1355,6 +1358,7 @@ const ssh_keyalg ssh_ecdsa_nistp384 = {
.has_private = ecdsa_has_private,
.cache_str = ecdsa_cache_str,
.components = ecdsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = ec_shared_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,
@ -1385,6 +1389,7 @@ const ssh_keyalg ssh_ecdsa_nistp521 = {
.has_private = ecdsa_has_private,
.cache_str = ecdsa_cache_str,
.components = ecdsa_components,
.base_key = nullkey_base_key,
.pubkey_bits = ec_shared_pubkey_bits,
.supported_flags = nullkey_supported_flags,
.alternate_ssh_id = nullkey_alternate_ssh_id,

View File

@ -185,13 +185,21 @@ 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 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,
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 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
@ -233,9 +241,14 @@ static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self,
.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, \
.pubkey_bits = opensshcert_pubkey_bits, \
.supported_flags = opensshcert_supported_flags, \
.alternate_ssh_id = opensshcert_alternate_ssh_id, \
.related_alg = opensshcert_related_alg, \
.ssh_id = ssh_id_prefix "-cert-v01@openssh.com", \
.cache_id = NULL, \
.extra = &opensshcert_##name##_extra, \
@ -419,14 +432,27 @@ static void opensshcert_freekey(ssh_key *key)
sfree(ck);
}
static ssh_key *opensshcert_ca_pub_key(opensshcert_key *ck, ptrlen *algname)
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(ca_keyblob);
*algname = pubkey_blob_to_alg_name(alg_source);
const ssh_keyalg *ca_alg = pubkey_blob_to_alg(ca_keyblob);
const ssh_keyalg *ca_alg = pubkey_blob_to_alg(alg_source);
if (!ca_alg)
return NULL; /* don't even recognise the certifying key type */
@ -494,6 +520,18 @@ static void opensshcert_openssh_blob(ssh_key *key, BinarySink *bs)
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);
@ -588,7 +626,8 @@ static key_components *opensshcert_components(ssh_key *key)
ck->signature_key));
ptrlen ca_algname;
ssh_key *ca_key = opensshcert_ca_pub_key(ck, &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) {
@ -638,12 +677,186 @@ static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self,
return self->ssh_id;
}
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,
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);
/* FIXME: here we should 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-*) */
ca_key = opensshcert_ca_pub_key(ck, signature, NULL);
if (!ca_key) {
put_fmt(error, "Certificate's signing key is invalid");
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

View File

@ -878,6 +878,7 @@ static const struct ssh2_rsa_extra
.has_private = rsa2_has_private, \
.cache_str = rsa2_cache_str, \
.components = rsa2_components, \
.base_key = nullkey_base_key, \
.pubkey_bits = rsa2_pubkey_bits, \
.cache_id = "rsa2"

23
ssh.h
View File

@ -844,11 +844,20 @@ struct ssh_keyalg {
bool (*has_private) (ssh_key *key);
char *(*cache_str) (ssh_key *key);
key_components *(*components) (ssh_key *key);
ssh_key *(*base_key) (ssh_key *key); /* does not confer ownership */
/* The following methods can be NULL if !is_certificate */
void (*ca_public_blob)(ssh_key *key, BinarySink *);
bool (*check_cert)(ssh_key *key, bool host, ptrlen principal,
uint64_t time, BinarySink *error);
void (*cert_id_string)(ssh_key *key, BinarySink *);
/* 'Class methods' that don't deal with an ssh_key at all */
int (*pubkey_bits) (const ssh_keyalg *self, ptrlen blob);
unsigned (*supported_flags) (const ssh_keyalg *self);
const char *(*alternate_ssh_id) (const ssh_keyalg *self, unsigned flags);
/* The following methods can be NULL if !is_certificate */
const ssh_keyalg *(*related_alg)(const ssh_keyalg *self,
const ssh_keyalg *base);
/* Constant data fields giving information about the key type */
const char *ssh_id; /* string identifier in the SSH protocol */
@ -887,6 +896,16 @@ static inline char *ssh_key_cache_str(ssh_key *key)
{ return key->vt->cache_str(key); }
static inline key_components *ssh_key_components(ssh_key *key)
{ return key->vt->components(key); }
static inline ssh_key *ssh_key_base_key(ssh_key *key)
{ return key->vt->base_key(key); }
static inline void ssh_key_ca_public_blob(ssh_key *key, BinarySink *bs)
{ key->vt->ca_public_blob(key, bs); }
static inline void ssh_key_cert_id_string(ssh_key *key, BinarySink *bs)
{ key->vt->cert_id_string(key, bs); }
static inline bool ssh_key_check_cert(
ssh_key *key, bool host, ptrlen principal, uint64_t time,
BinarySink *error)
{ return key->vt->check_cert(key, host, principal, time, error); }
static inline int ssh_key_public_bits(const ssh_keyalg *self, ptrlen blob)
{ return self->pubkey_bits(self, blob); }
static inline const ssh_keyalg *ssh_key_alg(ssh_key *key)
@ -902,10 +921,14 @@ static inline const unsigned ssh_keyalg_supported_flags(const ssh_keyalg *self)
static inline const char *ssh_keyalg_alternate_ssh_id(
const ssh_keyalg *self, unsigned flags)
{ return self->alternate_ssh_id(self, flags); }
static inline const ssh_keyalg *ssh_keyalg_related_alg(
const ssh_keyalg *self, const ssh_keyalg *base)
{ return self->related_alg(self, base); }
/* Stub functions shared between multiple key types */
unsigned nullkey_supported_flags(const ssh_keyalg *self);
const char *nullkey_alternate_ssh_id(const ssh_keyalg *self, unsigned flags);
ssh_key *nullkey_base_key(ssh_key *key);
/* Utility functions implemented centrally */
ssh_key *ssh_key_clone(ssh_key *key);

View File

@ -302,6 +302,14 @@ FUNC(void, ssh_key_openssh_blob, ARG(val_key, key),
FUNC(val_string_asciz, ssh_key_cache_str, ARG(val_key, key))
FUNC(val_keycomponents, ssh_key_components, ARG(val_key, key))
FUNC(uint, ssh_key_public_bits, ARG(keyalg, self), ARG(val_string_ptrlen, blob))
FUNC_WRAPPED(val_key, ssh_key_base_key, ARG(val_key, key))
FUNC_WRAPPED(void, ssh_key_ca_public_blob, ARG(val_key, key),
ARG(out_val_string_binarysink, blob))
FUNC_WRAPPED(void, ssh_key_cert_id_string, ARG(val_key, key),
ARG(out_val_string_binarysink, blob))
FUNC_WRAPPED(boolean, ssh_key_check_cert, ARG(val_key, key),
ARG(boolean, host), ARG(val_string_ptrlen, principal),
ARG(uint, time), ARG(out_val_string_binarysink, error))
/*
* Accessors to retrieve the innards of a 'key_components'.

View File

@ -806,6 +806,39 @@ strbuf *ssh2_mac_genresult_wrapper(ssh2_mac *m)
return sb;
}
ssh_key *ssh_key_base_key_wrapper(ssh_key *key)
{
/* To avoid having to explain the borrowed reference to Python,
* just clone the key unconditionally */
return ssh_key_clone(ssh_key_base_key(key));
}
void ssh_key_ca_public_blob_wrapper(ssh_key *key, BinarySink *out)
{
/* Wrap to avoid null-pointer dereference */
if (!key->vt->is_certificate)
fatal_error("ssh_key_ca_public_blob: needs a certificate");
ssh_key_ca_public_blob(key, out);
}
void ssh_key_cert_id_string_wrapper(ssh_key *key, BinarySink *out)
{
/* Wrap to avoid null-pointer dereference */
if (!key->vt->is_certificate)
fatal_error("ssh_key_cert_id_string: needs a certificate");
ssh_key_cert_id_string(key, out);
}
static bool ssh_key_check_cert_wrapper(
ssh_key *key, bool host, ptrlen principal, uint64_t time,
BinarySink *error)
{
/* Wrap to avoid null-pointer dereference */
if (!key->vt->is_certificate)
fatal_error("ssh_key_cert_id_string: needs a certificate");
return ssh_key_check_cert(key, host, principal, time, error);
}
bool dh_validate_f_wrapper(dh_ctx *dh, mp_int *f)
{
return dh_validate_f(dh, f) == NULL;

View File

@ -11,3 +11,9 @@ const char *nullkey_alternate_ssh_id(const ssh_keyalg *self, unsigned flags)
/* There are no alternate ids */
return self->ssh_id;
}
ssh_key *nullkey_base_key(ssh_key *key)
{
/* When a key is not certified, it is its own base */
return key;
}