diff --git a/crypto/dsa.c b/crypto/dsa.c index 530bbe09..e304a38f 100644 --- a/crypto/dsa.c +++ b/crypto/dsa.c @@ -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, diff --git a/crypto/ecc-ssh.c b/crypto/ecc-ssh.c index 9f637c2e..17fde8e9 100644 --- a/crypto/ecc-ssh.c +++ b/crypto/ecc-ssh.c @@ -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, diff --git a/crypto/openssh-certs.c b/crypto/openssh-certs.c index bec7bca3..7f61e55a 100644 --- a/crypto/openssh-certs.c +++ b/crypto/openssh-certs.c @@ -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 diff --git a/crypto/rsa.c b/crypto/rsa.c index 4087035d..635c9efb 100644 --- a/crypto/rsa.c +++ b/crypto/rsa.c @@ -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" diff --git a/ssh.h b/ssh.h index a57e6328..ecb5074c 100644 --- a/ssh.h +++ b/ssh.h @@ -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); diff --git a/test/testcrypt-func.h b/test/testcrypt-func.h index a7fad72e..0873c2ea 100644 --- a/test/testcrypt-func.h +++ b/test/testcrypt-func.h @@ -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'. diff --git a/test/testcrypt.c b/test/testcrypt.c index 566c95b9..c3c74784 100644 --- a/test/testcrypt.c +++ b/test/testcrypt.c @@ -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; diff --git a/utils/nullkey.c b/utils/nullkey.c index d4f2a691..1b01c891 100644 --- a/utils/nullkey.c +++ b/utils/nullkey.c @@ -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; +}