upstream: Account pubkey checks separately to auth attempts

Add a `PubkeyOptions max-pk-ok:nnnn` option to allow PK_OK tests (asking
whether the server might accept a given public key) that do not count
against MaxAuthTries, defaulting to 6 attempts.

After these attempts are exhausted, futher attempts count as failed
authentications against MaxAuthTries.

ok markus, deraadt

OpenBSD-Commit-ID: 4a02d2f303f4d83c10871221dce1db1f9fe2936f
This commit is contained in:
djm@openbsd.org authored and Damien Miller committed 2026-09-16 10:49:38 +10:00
1 parent 8895b5ae1b
commit 14843e73b9
6 files changed
+102 -13

No files matched your search

+3 -1
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: auth.h,v 1.110 2026/07/06 07:44:48 djm Exp $ */
/* $OpenBSD: auth.h,v 1.111 2026/09/16 00:16:52 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
@@ -59,6 +59,8 @@ struct Authctxt {
int valid; /* user exists and is allowed to login */
int attempt;
int failures;
int pk_ok_failures;
int auth_failure_already_counted;
int server_caused_failure;
int force_pwchange;
char *user; /* username sent by the client */
+14 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: auth2-pubkey.c,v 1.127 2026/07/30 03:37:39 djm Exp $ */
/* $OpenBSD: auth2-pubkey.c,v 1.128 2026/09/16 00:16:52 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2010 Damien Miller. All rights reserved.
@@ -291,7 +291,19 @@ userauth_pubkey(struct ssh *ssh, const char *method)
(r = ssh_packet_write_wait(ssh)) != 0)
fatal_fr(r, "send packet");
authctxt->postponed = 1;
}
} else {
/*
* Don't count this as an authentication failure
* unless we have already used up the separate
* max-pk-ok budget (if any).
*/
if (authctxt->pk_ok_failures++ < options.max_pubkey_ok)
authctxt->auth_failure_already_counted = 1;
debug3_f("pubkey test %d of %d%s",
authctxt->pk_ok_failures, options.max_pubkey_ok,
authctxt->auth_failure_already_counted ?
"" : ": treating as authentication failure");
}
}
done:
if (authenticated == 1 && auth_activate_options(ssh, authopts) != 0) {
+3 -1
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: auth2.c,v 1.174 2026/07/06 07:44:48 djm Exp $ */
/* $OpenBSD: auth2.c,v 1.175 2026/09/16 00:16:52 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
*
@@ -344,6 +344,7 @@ input_userauth_request(int type, uint32_t seq, struct ssh *ssh)
auth2_authctxt_reset_info(authctxt);
authctxt->postponed = 0;
authctxt->server_caused_failure = 0;
authctxt->auth_failure_already_counted = 0;
/* try to authenticate user */
m = authmethod_lookup(authctxt, method);
@@ -448,6 +449,7 @@ userauth_finish(struct ssh *ssh, int authenticated, const char *packet_method,
} else {
/* Allow initial try of "none" auth without failure penalty */
if (!partial && !authctxt->server_caused_failure &&
!authctxt->auth_failure_already_counted &&
(authctxt->attempt > 1 || strcmp(method, "none") != 0))
authctxt->failures++;
if (authctxt->failures >= options.max_authtries) {
+60 -4
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: servconf.c,v 1.453 2026/09/16 00:13:58 djm Exp $ */
/* $OpenBSD: servconf.c,v 1.454 2026/09/16 00:16:52 djm Exp $ */
/*
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
* All rights reserved
@@ -159,6 +159,9 @@ initialize_server_options(ServerOptions *options)
options->subsystem_name = NULL; \
options->subsystem_command = NULL; \
options->subsystem_args = NULL;
#define init_pubkey_auth_options(options) \
options->pubkey_auth_options = -1; \
options->max_pubkey_ok = -1;
#define init_timingsecret(options) \
options->timing_secret = 0;
@@ -407,6 +410,10 @@ fill_default_server_options(ServerOptions *options)
options->sshd_session_path = xstrdup(_PATH_SSHD_SESSION);
if (options->sshd_auth_path == NULL)
options->sshd_auth_path = xstrdup(_PATH_SSHD_AUTH);
if (options->pubkey_auth_options == -1) {
options->pubkey_auth_options = 0;
options->max_pubkey_ok = DEFAULT_AUTH_FAIL_MAX;
}
assemble_algorithms(options);
@@ -1403,6 +1410,7 @@ process_server_config_line_depth(ServerOptions *options, char *line,
case sPubkeyAuthOptions:
intptr = &options->pubkey_auth_options;
value = 0;
value2 = -1;
while ((arg = argv_next(&ac, &av)) != NULL) {
if (strcasecmp(arg, "none") == 0)
continue;
@@ -1410,14 +1418,24 @@ process_server_config_line_depth(ServerOptions *options, char *line,
value |= PUBKEYAUTH_TOUCH_REQUIRED;
else if (strcasecmp(arg, "verify-required") == 0)
value |= PUBKEYAUTH_VERIFY_REQUIRED;
else {
else if (strncasecmp(arg, "max-pk-ok:", 10) == 0) {
value2 = strtonum(arg + 10, 0, 255, &errstr);
if (errstr != NULL) {
error("%s line %d: bad %s max-pk-ok "
"value: %s", filename, linenum,
keyword, errstr);
goto out;
}
} else {
error("%s line %d: unsupported %s option %s",
filename, linenum, keyword, arg);
goto out;
}
}
if (*activep && *intptr == -1)
*intptr = value;
if (*activep && options->pubkey_auth_options == -1) {
options->pubkey_auth_options = value;
options->max_pubkey_ok = value2;
}
break;
#ifdef KRB5
@@ -3060,6 +3078,19 @@ serialise_subsystem(const ServerOptions *options, struct sshbuf *buf)
return 0;
}
static int
serialise_pubkey_auth_options(const ServerOptions *options, struct sshbuf *buf)
{
int r;
if ((r = serialise_s32(buf, options->pubkey_auth_options)) != 0 ||
(r = serialise_s32(buf, options->max_pubkey_ok)) != 0) {
error_fr(r, "serialise");
return r;
}
return 0;
}
static int
serialise_timingsecret(const ServerOptions *options, struct sshbuf *buf)
{
@@ -3594,6 +3625,19 @@ deserialise_subsystem(ServerOptions *options, struct sshbuf *buf)
return 0;
}
static int
deserialise_pubkey_auth_options(ServerOptions *options, struct sshbuf *buf)
{
int r;
if ((r = deserialise_s32(buf, &options->pubkey_auth_options)) != 0 ||
(r = deserialise_s32(buf, &options->max_pubkey_ok)) != 0) {
error_fr(r, "deserialise");
return r;
}
return 0;
}
static int
deserialise_timingsecret(ServerOptions *options, struct sshbuf *buf)
{
@@ -3768,6 +3812,7 @@ free_server_options(ServerOptions *options)
#define free_persourcenetblocksize(options)
#define free_persourcepenalties(options)
#define free_rekeylimit(options)
#define free_pubkey_auth_options(options)
#define free_timingsecret(options)
SSHD_CONFIG_ENTRIES
@@ -3900,6 +3945,15 @@ copy_subsystem(ServerOptions *dst, const ServerOptions *src)
dst->num_subsystems = src->num_subsystems;
}
static void
copy_pubkey_auth_options(ServerOptions *dst, const ServerOptions *src)
{
if (src->pubkey_auth_options != -1) {
dst->pubkey_auth_options = src->pubkey_auth_options;
dst->max_pubkey_ok = src->max_pubkey_ok;
}
}
/*
* Copy any supported values that are set.
*
@@ -4374,6 +4428,8 @@ dump_config(ServerOptions *o)
printf(" touch-required");
if (o->pubkey_auth_options & PUBKEYAUTH_VERIFY_REQUIRED)
printf(" verify-required");
if (o->max_pubkey_ok != -1)
printf(" max-pk-ok:%d", o->max_pubkey_ok);
printf("\n");
if (o->per_source_penalty.enabled) {
+5 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: servconf.h,v 1.180 2026/09/16 00:13:58 djm Exp $ */
/* $OpenBSD: servconf.h,v 1.181 2026/09/16 00:16:52 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -161,6 +161,7 @@ SSHCONF_CUSTOM(MaxStartups, maxstartups, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_CUSTOM(PerSourceNetBlockSize, persourcenetblocksize, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_CUSTOM(PerSourcePenalties, persourcepenalties, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_CUSTOM(RekeyLimit, rekeylimit, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_CUSTOM(PubkeyAuthOptions, pubkey_auth_options, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_NONCONF(timingsecret)
#define SSHD_CONFIG_ENTRIES_MAIN \
@@ -192,7 +193,6 @@ SSHCONF_INTFLAG(hostbased_uses_name_from_packet_only, HostbasedUsesNameFromPacke
SSHCONF_STRING(hostbased_accepted_algos, HostbasedAcceptedAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_STRING(hostkeyalgorithms, HostKeyAlgorithms, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_STRING(ca_sign_algorithms, CASignatureAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_INT(pubkey_auth_options, PubkeyAuthOptions, SSHCFG_ALL, NULL, 0, SSHCFG_COPY_MATCH) \
SSHCONF_INTFLAG(pubkey_authentication, PubkeyAuthentication, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \
SSHCONF_STRING(pubkey_accepted_algos, PubkeyAcceptedAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_INTFLAG(password_authentication, PasswordAuthentication, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \
@@ -394,6 +394,9 @@ typedef struct ServerOptions {
/* RekeyLimit */
int64_t rekey_limit;
int rekey_interval;
/* PubkeyAuthOptions */
int pubkey_auth_options;
int max_pubkey_ok;
/* Passed by config but not keyword for this */
uint64_t timing_secret;
} ServerOptions;
+17 -3
View File
@@ -33,7 +33,7 @@
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\"
.\" $OpenBSD: sshd_config.5,v 1.404 2026/09/16 00:13:58 djm Exp $
.\" $OpenBSD: sshd_config.5,v 1.405 2026/09/16 00:16:52 djm Exp $
.Dd $Mdocdate: September 16 2026 $
.Dt SSHD_CONFIG 5
.Os
@@ -1782,9 +1782,10 @@ Sets one or more public key authentication options.
The supported keywords are:
.Cm none
(the default; indicating no additional options are enabled),
.Cm touch-required
.Cm touch-required ,
.Cm verify-required
and
.Cm verify-required .
.Cm max-pk-ok .
.Pp
The
.Cm touch-required
@@ -1812,6 +1813,19 @@ Neither the
or
.Cm verify-required
options have any effect for other, non-FIDO, public key types.
.Pp
The
.Cm max-pk-ok
option takes a numeric argument separated by a colon character, e.g.
.Sq max-pk-ok:4
to specify the number of public key query attempts are permitted
before the attempts are considered failed authentications that count against
.Cm MaxAuthTries .
Public key queries are a SSH protocol mechanism that allow a client to test
whether a server accepts a given public key before an actual authentication
attempt, to avoid the need to enter PINs/key passphrases or touch FIDO
authenticators unnecessarily.
The default is 6.
.It Cm PubkeyAuthentication
Specifies whether public key authentication is allowed.
The default is