[openssh-commits] [openssh] 03/14: upstream: Account pubkey checks separately to auth attempts
git+noreply at mindrot.org
git+noreply at mindrot.org
Wed Sep 16 11:15:57 AEST 2026
This is an automated email from the git hooks/post-receive script.
djm pushed a commit to branch master
in repository openssh.
commit 14843e73b970e248b785d84df0e54ae9f3992cff
Author: djm at openbsd.org <djm at openbsd.org>
AuthorDate: Wed Sep 16 00:16:52 2026 +0000
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
---
auth.h | 4 +++-
auth2-pubkey.c | 16 +++++++++++++--
auth2.c | 4 +++-
servconf.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
servconf.h | 7 +++++--
sshd_config.5 | 20 +++++++++++++++---
6 files changed, 102 insertions(+), 13 deletions(-)
diff --git a/auth.h b/auth.h
index 0f11458ca..17227cffb 100644
--- a/auth.h
+++ b/auth.h
@@ -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 */
diff --git a/auth2-pubkey.c b/auth2-pubkey.c
index c15b1ba71..97d248795 100644
--- a/auth2-pubkey.c
+++ b/auth2-pubkey.c
@@ -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) {
diff --git a/auth2.c b/auth2.c
index 3f353a719..6971b933b 100644
--- a/auth2.c
+++ b/auth2.c
@@ -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) {
diff --git a/servconf.c b/servconf.c
index d6f344989..a5bf3479a 100644
--- a/servconf.c
+++ b/servconf.c
@@ -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 at 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) {
diff --git a/servconf.h b/servconf.h
index a394274e2..089d600ad 100644
--- a/servconf.h
+++ b/servconf.h
@@ -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 at 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;
diff --git a/sshd_config.5 b/sshd_config.5
index 1bb4a3791..0d9f88061 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -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
--
To stop receiving notification emails like this one, please contact
djm at mindrot.org.
More information about the openssh-commits
mailing list