Files
openssh-portable/auth2-pubkeyfile.c
djm@openbsd.org 5166b6cbf2 upstream: When certificate support was added to OpenSSH,
certificates were originally specified to represent any principal if the
principals list was empty.

This was, in retrospect, a mistake as it created a fail-open
situation if a CA could be convinced to accidentally sign a
certificate with no principals. This actually happened in a 3rd-
party CA product (CVE-2024-7594).

Somewhat fortunately, the main pathway for using certificates in
sshd (TrustedUserCAKeys) never supported empty-principals
certificates, so the blast radius of such mistakes was
substantially reduced.

This change removes this footcannon and requires all certificates
include principals sections. It also fixes interpretation of
wildcard principals, and properly enables them for host
certificates only.

This is a behaviour change that will permanently break uses of
certificates with empty principals sections.

ok markus@

OpenBSD-Commit-ID: 0a901f03c567c100724a492cf91e02939904712e
2025-12-22 12:51:24 +11:00

510 lines
14 KiB
C

/* $OpenBSD: auth2-pubkeyfile.c,v 1.7 2025/12/22 01:49:03 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2010 Damien Miller. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "includes.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "ssh.h"
#include "log.h"
#include "misc.h"
#include "sshkey.h"
#include "digest.h"
#include "hostfile.h"
#include "auth.h"
#include "auth-options.h"
#include "authfile.h"
#include "match.h"
#include "ssherr.h"
int
auth_authorise_keyopts(struct passwd *pw, struct sshauthopt *opts,
int allow_cert_authority, const char *remote_ip, const char *remote_host,
const char *loc)
{
time_t now = time(NULL);
char buf[64];
/*
* Check keys/principals file expiry time.
* NB. validity interval in certificate is handled elsewhere.
*/
if (opts->valid_before && now > 0 &&
opts->valid_before < (uint64_t)now) {
format_absolute_time(opts->valid_before, buf, sizeof(buf));
debug("%s: entry expired at %s", loc, buf);
auth_debug_add("%s: entry expired at %s", loc, buf);
return -1;
}
/* Consistency checks */
if (opts->cert_principals != NULL && !opts->cert_authority) {
debug("%s: principals on non-CA key", loc);
auth_debug_add("%s: principals on non-CA key", loc);
/* deny access */
return -1;
}
/* cert-authority flag isn't valid in authorized_principals files */
if (!allow_cert_authority && opts->cert_authority) {
debug("%s: cert-authority flag invalid here", loc);
auth_debug_add("%s: cert-authority flag invalid here", loc);
/* deny access */
return -1;
}
/* Perform from= checks */
if (opts->required_from_host_keys != NULL) {
switch (match_host_and_ip(remote_host, remote_ip,
opts->required_from_host_keys )) {
case 1:
/* Host name matches. */
break;
case -1:
default:
debug("%s: invalid from criteria", loc);
auth_debug_add("%s: invalid from criteria", loc);
/* FALLTHROUGH */
case 0:
logit("%s: Authentication tried for %.100s with "
"correct key but not from a permitted "
"host (host=%.200s, ip=%.200s, required=%.200s).",
loc, pw->pw_name, remote_host, remote_ip,
opts->required_from_host_keys);
auth_debug_add("%s: Your host '%.200s' is not "
"permitted to use this key for login.",
loc, remote_host);
/* deny access */
return -1;
}
}
/* Check source-address restriction from certificate */
if (opts->required_from_host_cert != NULL) {
switch (addr_match_cidr_list(remote_ip,
opts->required_from_host_cert)) {
case 1:
/* accepted */
break;
case -1:
default:
/* invalid */
error("%s: Certificate source-address invalid", loc);
/* FALLTHROUGH */
case 0:
logit("%s: Authentication tried for %.100s with valid "
"certificate but not from a permitted source "
"address (%.200s).", loc, pw->pw_name, remote_ip);
auth_debug_add("%s: Your address '%.200s' is not "
"permitted to use this certificate for login.",
loc, remote_ip);
return -1;
}
}
/*
*
* XXX this is spammy. We should report remotely only for keys
* that are successful in actual auth attempts, and not PK_OK
* tests.
*/
auth_log_authopts(loc, opts, 1);
return 0;
}
static int
match_principals_option(const char *principal_list, struct sshkey_cert *cert)
{
char *result;
u_int i;
/* XXX percent_expand() sequences for authorized_principals? */
for (i = 0; i < cert->nprincipals; i++) {
if ((result = match_list(cert->principals[i],
principal_list, NULL)) != NULL) {
debug3("matched principal from key options \"%.100s\"",
result);
free(result);
return 1;
}
}
return 0;
}
/*
* Process a single authorized_principals format line. Returns 0 and sets
* authoptsp is principal is authorised, -1 otherwise. "loc" is used as a
* log preamble for file/line information.
*/
int
auth_check_principals_line(char *cp, const struct sshkey_cert *cert,
const char *loc, struct sshauthopt **authoptsp)
{
u_int i, found = 0;
char *ep, *line_opts;
const char *reason = NULL;
struct sshauthopt *opts = NULL;
if (authoptsp != NULL)
*authoptsp = NULL;
/* Trim trailing whitespace. */
ep = cp + strlen(cp) - 1;
while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t'))
*ep-- = '\0';
/*
* If the line has internal whitespace then assume it has
* key options.
*/
line_opts = NULL;
if ((ep = strrchr(cp, ' ')) != NULL ||
(ep = strrchr(cp, '\t')) != NULL) {
for (; *ep == ' ' || *ep == '\t'; ep++)
;
line_opts = cp;
cp = ep;
}
if ((opts = sshauthopt_parse(line_opts, &reason)) == NULL) {
debug("%s: bad principals options: %s", loc, reason);
auth_debug_add("%s: bad principals options: %s", loc, reason);
return -1;
}
/* Check principals in cert against those on line */
for (i = 0; i < cert->nprincipals; i++) {
if (strcmp(cp, cert->principals[i]) != 0)
continue;
debug3("%s: matched principal \"%.100s\"",
loc, cert->principals[i]);
found = 1;
}
if (found && authoptsp != NULL) {
*authoptsp = opts;
opts = NULL;
}
sshauthopt_free(opts);
return found ? 0 : -1;
}
int
auth_process_principals(FILE *f, const char *file,
const struct sshkey_cert *cert, struct sshauthopt **authoptsp)
{
char loc[256], *line = NULL, *cp, *ep;
size_t linesize = 0;
u_long linenum = 0, nonblank = 0;
u_int found_principal = 0;
if (authoptsp != NULL)
*authoptsp = NULL;
while (getline(&line, &linesize, f) != -1) {
linenum++;
/* Always consume entire input */
if (found_principal)
continue;
/* Skip leading whitespace. */
for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
;
/* Skip blank and comment lines. */
if ((ep = strchr(cp, '#')) != NULL)
*ep = '\0';
if (!*cp || *cp == '\n')
continue;
nonblank++;
snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
if (auth_check_principals_line(cp, cert, loc, authoptsp) == 0)
found_principal = 1;
}
debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
free(line);
return found_principal;
}
/*
* Check a single line of an authorized_keys-format file. Returns 0 if key
* matches, -1 otherwise. Will return key/cert options via *authoptsp
* on success. "loc" is used as file/line location in log messages.
*/
int
auth_check_authkey_line(struct passwd *pw, struct sshkey *key,
char *cp, const char *remote_ip, const char *remote_host, const char *loc,
struct sshauthopt **authoptsp)
{
int want_keytype = sshkey_is_cert(key) ? KEY_UNSPEC : key->type;
struct sshkey *found = NULL;
struct sshauthopt *keyopts = NULL, *certopts = NULL, *finalopts = NULL;
char *key_options = NULL, *fp = NULL;
const char *reason = NULL;
int ret = -1;
if (authoptsp != NULL)
*authoptsp = NULL;
if ((found = sshkey_new(want_keytype)) == NULL) {
debug3_f("keytype %d failed", want_keytype);
goto out;
}
/* XXX djm: peek at key type in line and skip if unwanted */
if (sshkey_read(found, &cp) != 0) {
/* no key? check for options */
debug2("%s: check options: '%s'", loc, cp);
key_options = cp;
if (sshkey_advance_past_options(&cp) != 0) {
reason = "invalid key option string";
goto fail_reason;
}
skip_space(&cp);
if (sshkey_read(found, &cp) != 0) {
/* still no key? advance to next line*/
debug2("%s: advance: '%s'", loc, cp);
goto out;
}
}
/* Parse key options now; we need to know if this is a CA key */
if ((keyopts = sshauthopt_parse(key_options, &reason)) == NULL) {
debug("%s: bad key options: %s", loc, reason);
auth_debug_add("%s: bad key options: %s", loc, reason);
goto out;
}
/* Ignore keys that don't match or incorrectly marked as CAs */
if (sshkey_is_cert(key)) {
/* Certificate; check signature key against CA */
if (!sshkey_equal(found, key->cert->signature_key) ||
!keyopts->cert_authority)
goto out;
} else {
/* Plain key: check it against key found in file */
if (!sshkey_equal(found, key) || keyopts->cert_authority)
goto out;
}
/* We have a candidate key, perform authorisation checks */
if ((fp = sshkey_fingerprint(found,
SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL)
fatal_f("fingerprint failed");
debug("%s: matching %s found: %s %s", loc,
sshkey_is_cert(key) ? "CA" : "key", sshkey_type(found), fp);
if (auth_authorise_keyopts(pw, keyopts,
sshkey_is_cert(key), remote_ip, remote_host, loc) != 0) {
reason = "Refused by key options";
goto fail_reason;
}
/* That's all we need for plain keys. */
if (!sshkey_is_cert(key)) {
verbose("Accepted key %s %s found at %s",
sshkey_type(found), fp, loc);
finalopts = keyopts;
keyopts = NULL;
goto success;
}
/*
* Additional authorisation for certificates.
*/
/* Parse and check options present in certificate */
if ((certopts = sshauthopt_from_cert(key)) == NULL) {
reason = "Invalid certificate options";
goto cert_fail_reason;
}
if (auth_authorise_keyopts(pw, certopts, 0,
remote_ip, remote_host, loc) != 0) {
reason = "Refused by certificate options";
goto cert_fail_reason;
}
if ((finalopts = sshauthopt_merge(keyopts, certopts, &reason)) == NULL)
goto cert_fail_reason;
/*
* If the user has specified a list of principals as
* a key option, then prefer that list to matching
* their username in the certificate principals list.
*/
if (keyopts->cert_principals != NULL &&
!match_principals_option(keyopts->cert_principals, key->cert)) {
reason = "Certificate does not contain an authorized principal";
goto cert_fail_reason;
}
if (sshkey_cert_check_authority_now(key, 0, 0,
keyopts->cert_principals == NULL ? pw->pw_name : NULL,
&reason) != 0)
goto cert_fail_reason;
verbose("Accepted certificate ID \"%s\" (serial %llu) "
"signed by CA %s %s found at %s",
key->cert->key_id,
(unsigned long long)key->cert->serial,
sshkey_type(found), fp, loc);
success:
if (finalopts == NULL)
fatal_f("internal error: missing options");
if (authoptsp != NULL) {
*authoptsp = finalopts;
finalopts = NULL;
}
/* success */
ret = 0;
goto out;
cert_fail_reason:
error("Refusing certificate ID \"%s\" serial=%llu "
"signed by %s CA %s via %s: %s", key->cert->key_id,
(unsigned long long)key->cert->serial,
sshkey_type(key->cert->signature_key), fp, loc, reason);
auth_debug_add("Refused Certificate ID \"%s\" serial=%llu: %s",
key->cert->key_id, (unsigned long long)key->cert->serial, reason);
goto out;
fail_reason:
error("%s at %s", reason, loc);
auth_debug_add("%s", reason);
out:
free(fp);
sshauthopt_free(keyopts);
sshauthopt_free(certopts);
sshauthopt_free(finalopts);
sshkey_free(found);
return ret;
}
/*
* Checks whether key is allowed in authorized_keys-format file,
* returns 1 if the key is allowed or 0 otherwise.
*/
int
auth_check_authkeys_file(struct passwd *pw, FILE *f, char *file,
struct sshkey *key, const char *remote_ip,
const char *remote_host, struct sshauthopt **authoptsp)
{
char *cp, *line = NULL, loc[256];
size_t linesize = 0;
int found_key = 0;
u_long linenum = 0, nonblank = 0;
if (authoptsp != NULL)
*authoptsp = NULL;
while (getline(&line, &linesize, f) != -1) {
linenum++;
/* Always consume entire file */
if (found_key)
continue;
/* Skip leading whitespace, empty and comment lines. */
cp = line;
skip_space(&cp);
if (!*cp || *cp == '\n' || *cp == '#')
continue;
nonblank++;
snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
if (auth_check_authkey_line(pw, key, cp,
remote_ip, remote_host, loc, authoptsp) == 0)
found_key = 1;
}
free(line);
debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
return found_key;
}
static FILE *
auth_openfile(const char *file, struct passwd *pw, int strict_modes,
int log_missing, char *file_type)
{
char line[1024];
struct stat st;
int fd;
FILE *f;
if ((fd = open(file, O_RDONLY|O_NONBLOCK)) == -1) {
if (errno != ENOENT) {
logit("Could not open user '%s' %s '%s': %s",
pw->pw_name, file_type, file, strerror(errno));
} else if (log_missing) {
debug("Could not open user '%s' %s '%s': %s",
pw->pw_name, file_type, file, strerror(errno));
}
return NULL;
}
if (fstat(fd, &st) == -1) {
close(fd);
return NULL;
}
if (!S_ISREG(st.st_mode)) {
logit("User '%s' %s '%s' is not a regular file",
pw->pw_name, file_type, file);
close(fd);
return NULL;
}
unset_nonblock(fd);
if ((f = fdopen(fd, "r")) == NULL) {
close(fd);
return NULL;
}
if (strict_modes &&
safe_path_fd(fileno(f), file, pw, line, sizeof(line)) != 0) {
fclose(f);
logit("Authentication refused: %s", line);
auth_debug_add("Ignored %s: %s", file_type, line);
return NULL;
}
return f;
}
FILE *
auth_openkeyfile(const char *file, struct passwd *pw, int strict_modes)
{
return auth_openfile(file, pw, strict_modes, 1, "authorized keys");
}
FILE *
auth_openprincipals(const char *file, struct passwd *pw, int strict_modes)
{
return auth_openfile(file, pw, strict_modes, 0,
"authorized principals");
}