RFC: PROTOCOL.authrec, a structured authentication record from sshd
Avinash Duduskar
avinash.duduskar at gmail.com
Sun Aug 23 17:35:45 AEST 2026
Here is the revised spec and a prototype patch implementing it.
The spec changes are the ones from djm's review. One correction to
the July draft: it said method_info carries the sk signature
counter and the hostbased client user and host. Tested on stock
10.4 with a FIDO key, neither reaches the file; the GSSAPI
display name is the only case populated today. The spec now claims
only that.
The patch adds the marshaller, ExposeAuthInfo=structured, the
sshd_config.5 text, a unit test for the outer framing, and the
spec as PROTOCOL.authrec. ExposeAuthInfo=yes is untouched. Two
changes to review: kex->name is added to the keystate blob,
otherwise NULL in the process that writes the file, and Authctxt
gains the method sequence, filled from auth2_update_session_info().
Blob skew during an upgrade fails closed both ways.
The section 6 example is a live record from this build.
Tested on Arch Linux (7.1.6-arch1-1), OpenBSD 7.8 and FreeBSD 14.2.
ASan shows no leaks over a 200-step accumulator run.
The AKC record and the gates stay separate follow-ups.
Avi
-- >8 --
From: Avinash Duduskar <avinash.duduskar at gmail.com>
Subject: [PATCH] Add a structured authentication record (PROTOCOL.authrec)
ExposeAuthInfo gains a third value, "structured", which writes a
binary record of the completed userauth to the $SSH_USER_AUTH file
in place of the legacy text. The format is documented in
PROTOCOL.authrec: an sshbuf wire format with a magic preamble and a
version, carrying a session block (user, uid, gid, home, auth time,
one entry per satisfied authentication step with the step's key)
and a transport block (negotiated algorithms, strict KEX, host key
fingerprint, endpoints, version strings, rdomain).
The method sequence accumulates on the Authctxt next to
session_info, filled from auth2_update_session_info(), so only
steps that succeeded outright or satisfied a step of
AuthenticationMethods are recorded.
kex->name now rides the keystate blob sshd-auth hands sshd-session
at userauth completion; it is otherwise NULL in the process that
writes the file. packet.c also gains an accessor for the active
algorithm names.
ExposeAuthInfo=yes and the legacy text format are unchanged. A new
unit test covers the record's outer framing: magic and version
checks, truncation at every byte, and trailing data.
---
Makefile.in | 17 +-
PROTOCOL.authrec | 295 +++++++++++++++++++++++++++++
auth.h | 1 +
auth2.c | 4 +
authrec.c | 222 ++++++++++++++++++++++
authrec.h | 57 ++++++
packet.c | 22 ++-
packet.h | 2 +
regress/Makefile | 1 +
regress/unittests/Makefile | 2 +-
regress/unittests/authrec/Makefile | 44 +++++
regress/unittests/authrec/tests.c | 123 ++++++++++++
servconf.c | 11 +-
servconf.h | 6 +-
session.c | 22 ++-
sshd_config.5 | 6 +
16 files changed, 825 insertions(+), 10 deletions(-)
diff --git a/Makefile.in b/Makefile.in
index da59bcc10..e405ea30a 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -123,7 +123,7 @@ SSHDOBJS=sshd.o \
dns.o fatal.o compat.o utf8.o authfd.o canohost.o \
$(P11OBJS) $(SKOBJS)
-SSHD_SESSION_OBJS=sshd-session.o auth-rhosts.o auth-passwd.o \
+SSHD_SESSION_OBJS=sshd-session.o authrec.o auth-rhosts.o auth-passwd.o \
audit.o audit-bsm.o audit-linux.o platform.o \
sshpty.o sshlogin.o servconf.o serverloop.o \
auth.o auth2.o auth2-methods.o auth-options.o session.o \
@@ -137,6 +137,7 @@ SSHD_SESSION_OBJS=sshd-session.o auth-rhosts.o auth-passwd.o \
uidswap.o platform-listen.o $(P11OBJS) $(SKOBJS)
SSHD_AUTH_OBJS=sshd-auth.o \
+ authrec.o \
auth2-methods.o \
auth-rhosts.o auth-passwd.o sshpty.o sshlogin.o servconf.o \
serverloop.o auth.o auth2.o auth-options.o session.o auth2-chall.o \
@@ -306,6 +307,8 @@ clean: regressclean
rm -f regress/unittests/test_helper/*.o
rm -f regress/unittests/authopt/*.o
rm -f regress/unittests/authopt/test_authopt$(EXEEXT)
+ rm -f regress/unittests/authrec/*.o
+ rm -f regress/unittests/authrec/test_authrec$(EXEEXT)
rm -f regress/unittests/bitmap/*.o
rm -f regress/unittests/bitmap/test_bitmap$(EXEEXT)
rm -f regress/unittests/conversion/*.o
@@ -644,6 +647,17 @@ regress/unittests/bitmap/test_bitmap$(EXEEXT): ${UNITTESTS_TEST_BITMAP_OBJS} \
regress/unittests/test_helper/libtest_helper.a \
-lssh -lopenbsd-compat -lssh -lopenbsd-compat $(TESTLIBS)
+UNITTESTS_TEST_AUTHREC_OBJS=\
+ regress/unittests/authrec/tests.o \
+ authrec.o \
+ $(P11OBJS) $(SKOBJS)
+
+regress/unittests/authrec/test_authrec$(EXEEXT): ${UNITTESTS_TEST_AUTHREC_OBJS} \
+ regress/unittests/test_helper/libtest_helper.a libssh.a
+ $(LD) -o $@ $(LDFLAGS) $(UNITTESTS_TEST_AUTHREC_OBJS) \
+ regress/unittests/test_helper/libtest_helper.a \
+ -lssh -lopenbsd-compat -lssh -lopenbsd-compat $(TESTLIBS)
+
UNITTESTS_TEST_AUTHOPT_OBJS=\
regress/unittests/authopt/tests.o \
auth-options.o \
@@ -793,6 +807,7 @@ regress-binaries: regress-prep $(LIBCOMPAT) \
regress-unit-binaries: regress-prep $(REGRESSLIBS) \
regress/unittests/authopt/test_authopt$(EXEEXT) \
+ regress/unittests/authrec/test_authrec$(EXEEXT) \
regress/unittests/bitmap/test_bitmap$(EXEEXT) \
regress/unittests/conversion/test_conversion$(EXEEXT) \
regress/unittests/hostkeys/test_hostkeys$(EXEEXT) \
diff --git a/PROTOCOL.authrec b/PROTOCOL.authrec
new file mode 100644
index 000000000..6a5b12643
--- /dev/null
+++ b/PROTOCOL.authrec
@@ -0,0 +1,295 @@
+This document describes a structured authentication record produced
+by sshd at userauth completion, for tooling that needs the facts sshd
+determined during userauth in a machine-readable form: audit
+pipelines, session policy hooks, identity-provider liveness checks.
+
+Version 1 defines the record and one delivery channel, an extension
+to the ExposeAuthInfo file mechanism shipped in OpenSSH 7.6. The wire
+format is delivery-agnostic and further channels may be added without
+changing the grammar. The encoding is sshbuf for OpenSSH-internal
+handling; a rendering tool for consumers that do not parse sshbuf is
+a property of that tool, not of this format.
+
+1. Wire format
+
+#define MAGIC_PREAMBLE "SSHAUTHR"
+#define FORMAT_VERSION 0x00000001
+
+ byte[8] MAGIC_PREAMBLE
+ uint32 FORMAT_VERSION
+ string contents
+
+Where "contents" carries:
+
+ string session_block
+ string transport_block
+
+Every element that encloses others is an SSH string and so carries its
+own length. There are no counts and no reserved fields; section 2
+describes how the format grows. Blocks that do not apply to the
+authentication event are present but empty, and a producer emits every
+field and block this document defines.
+
+The MAGIC_PREAMBLE ensures records cannot be confused with any other
+SSH wire artefact, or with the legacy text format written at the same
+path.
+
+1.1 Session block
+
+ string session_id
+ string user
+ uint32 uid
+ uint32 gid
+ string home
+ uint64 auth_time
+ string methods
+
+Where "session_id" is the canonical SSH session identifier from the
+first key exchange, "user" is the username the client requested,
+"home" is the matched account's home directory, "auth_time" is the
+time in seconds since 1970-01-01 00:00:00 UTC at userauth
+completion, and "methods" is the authentication method sequence
+below.
+
+"uid" and "gid" have no absent encoding, since every uint32 is a legal
+uid including zero. A version-1 producer must have resolved a passwd
+entry before emitting a record; sshd always has at the write site.
+
+"methods" holds one entry per satisfied authentication step, in the
+order satisfied, and is read until exhausted:
+
+ repeat until methods is exhausted:
+ string method_entry
+
+Each "method_entry" carries:
+
+ string method
+ string submethod
+ string method_info
+ string key_blob
+
+Only steps that succeeded appear: an entry is appended when an attempt
+succeeds outright or satisfies one step of AuthenticationMethods.
+Failed attempts are not recorded.
+
+Where "method" is the authentication method name, "publickey",
+"password", "hostbased", "keyboard-interactive" or "gssapi-with-mic"
+in current OpenSSH. "submethod" is the kbd-int device name for
+keyboard-interactive and empty for every other method; "bsdauth" and
+"pam" are the current device names. "method_info" mirrors the
+free-form per-method field ExposeAuthInfo=yes captures, which in
+current OpenSSH is the client principal's display name for GSSAPI and
+empty otherwise. "key_blob" is the step's public key in the usual SSH
+serialisation, or the full certificate for a certificate key, for the
+key-bearing methods and empty otherwise.
+
+None of these name sets is closed; see section 2.
+
+key_blob is the record's only carrier of key identity. A consumer
+needing a fingerprint, or a certificate's serial, key id, validity
+window or principals, parses them from the blob. For multi-step
+authentication the sequence carries one entry per step, mirroring the
+per-method lines the legacy file accumulates, so the record stays a
+superset of the text it replaces. Each entry is individually
+length-prefixed, so fields may be appended to an entry later.
+
+1.2 Transport block
+
+ string cipher_c2s
+ string cipher_s2c
+ string mac_c2s
+ string mac_s2c
+ string comp_c2s
+ string comp_s2c
+ string kex_algorithm
+ uint32 kex_options
+ string hostkey_algorithm
+ string hostkey_fingerprint
+ string client_addr
+ uint32 client_port
+ string server_addr
+ uint32 server_port
+ string client_version
+ string server_version
+ string rdomain
+
+Where the cipher, MAC and compression fields are the names
+negotiated in each direction and in effect at userauth completion,
+"mac_c2s" and "mac_s2c" being empty for AEAD
+ciphers that authenticate internally, and "comp_c2s" and "comp_s2c"
+being "none" when compression is disabled. "client_version" and
+"server_version" are the identification strings without CRLF, the V_C
+and V_S inputs to the KEX hash. "rdomain" is the routing domain the
+connection arrived on, as exposed to AuthorizedKeysCommand via %D, and
+empty if none.
+
+"kex_options" is a bitfield. Bit 0 is set if strict KEX
+(kex-strict-*-v00 at openssh.com, the Terrapin mitigation) was
+negotiated; no other bit is assigned. Producers write zero to
+unassigned bits and consumers ignore bits they do not recognise. A bit
+may only be given a meaning for which zero is the correct reading of
+records that predate its assignment.
+
+"hostkey_fingerprint" is the SHA-256 fingerprint, in OpenSSH
+"SHA256:..." form, of the host key sshd presented. It identifies which
+key was in use where several are in rotation. It is derived rather
+than carried whole because the host key blob is not in the record.
+
+The address and port fields carry what sshd holds. When the connection
+is not on a socket, as under sshd -i, sshd reports "UNKNOWN" for both
+addresses and 65535 for both ports. 65535 is a legal port, so a
+consumer distinguishing that case tests the address.
+
+2. Versioning and extensibility
+
+FORMAT_VERSION numbers begin at 1. Parsers MUST reject version 0
+and records with FORMAT_VERSION greater than they support.
+FORMAT_VERSION is incremented only for a change an older
+parser would misread: removing or repurposing a field or block, or
+changing the meaning of an assigned kex_options bit.
+
+Within a version the enclosing length prefix is what allows growth,
+and two rules apply at every enclosure:
+
+ - A parser that has read the fields it knows and finds bytes
+ remaining MUST ignore them.
+
+ - A parser that reaches the end of an enclosure before reading every
+ field it knows MUST treat the remainder as absent.
+
+The first lets an old parser read a new record, the second a new
+parser an old one. Neither applies to a malformed record: any field
+that cannot be read in full, whether a truncated scalar, a truncated
+length prefix, or a prefix claiming more bytes than remain in its
+enclosure, means the record MUST be rejected. A record ends at the
+end of contents; whether anything may follow it is a property of the
+delivery channel.
+
+These rules do not carry across a version bump, which exists precisely
+because tolerance could not absorb the change. A parser selects the
+grammar for the FORMAT_VERSION it reads; implementing versions below
+its maximum is optional.
+
+Appending fields to a method entry or a block, appending a block
+inside contents, and assigning an unused kex_options bit do not bump
+FORMAT_VERSION. Consumers may refuse a record whose contents exceed an
+implementation limit.
+
+The method, submethod and algorithm names are not closed sets; they
+gain members as OpenSSH gains methods, kbd-int devices and algorithms.
+A consumer that does not recognise a value MUST NOT treat it as
+absent, and MUST NOT treat it as acceptable: an unfamiliar value is
+one the consumer cannot evaluate, not one that has passed.
+
+3. Delivery channels
+
+3.1 File: ExposeAuthInfo=structured
+
+A new value for the sshd_config(5) ExposeAuthInfo keyword. When set,
+sshd writes the record to the path exported in $SSH_USER_AUTH, using
+the existing ExposeAuthInfo lifecycle. ExposeAuthInfo=yes is unchanged
+and continues to write the legacy text format. The file carries
+exactly one record, and a reader MUST reject a file carrying data
+after it.
+
+3.2 Additional channels
+
+Further channels may be added without changing the grammar. Each is a
+separate proposal:
+
+ - a partial record for AuthorizedKeysCommand and
+ AuthorizedPrincipalsCommand, which run before the record is
+ complete: the same grammar with only the blocks known at that
+ point populated. The satisfied steps and their keys are already
+ present when those commands run.
+
+ - a post-auth subprocess gate whose exit code decides session
+ establishment.
+
+ - a per-attempt gate at the start of userauth_finish(), able to
+ reject an otherwise-successful authentication.
+
+ - a PAM channel. This needs a binary-safe encoding, since the record
+ contains NUL bytes the existing C-string environment channel
+ cannot carry.
+
+ - a trusted channel for consumers outside sshd's own handoff, for
+ the case section 4 describes as unserved.
+
+4. Security considerations
+
+Whether a consumer may rely on a record depends on how it was
+delivered.
+
+Consumers sshd hands the record to directly, such as the gates in
+section 3.2, receive it with nothing under the authenticated user's
+control in between.
+
+The section 3.1 file is different. sshd creates it under the
+authenticated user's own uid, and the path arrives in an environment
+variable the user can repoint, so anything in-session can forge it. No
+ownership or mode check helps, because the adversary is the file's
+legitimate owner. A sudo policy, a PAM module, or any other component
+deciding what the user may do next MUST NOT treat the file as evidence
+of how the session authenticated. This is a property of the channel,
+unchanged since ExposeAuthInfo shipped in 7.6, and a trusted channel
+for such consumers is listed as a candidate in section 3.2.
+
+Parts of a record originate with the peer: client_version, the
+contents of method_info, and the key id, principal names and
+application string carried inside a key blob. A consumer rendering a
+record into a log, a terminal or a document MUST escape them for that
+destination.
+
+5. Fields not in version 1
+
+Deferred, and addable later without a version bump: the signature
+algorithm actually used and the sk-* signature counter and flags,
+which sshd does not retain to the write site; the authorized_keys
+options that applied to the matched key; a FIDO attestation block, for
+which no upstream mechanism exists yet; and authorization provenance,
+the matched principal, failed attempts, service name and GSSAPI ticket
+detail.
+
+Declined rather than deferred: parsed views of key and certificate
+material. All are recoverable from key_blob, and carrying both invites
+a record whose parsed fields disagree with its own blob.
+
+6. Example
+
+The record below was produced from a live loopback authentication: a
+single publickey step with an ed25519 user key, no certificate,
+mlkem768x25519-sha256 key exchange with strict KEX negotiated, and
+chacha20-poly1305 with no separate MAC. It is 462 bytes.
+
+ U1NIQVVUSFIAAAABAAABvgAAAKQAAAAgtUfpa/AltzIVZHuRsPk6tA8bLzc88xbT4G
+ oMovLORnUAAAAHc3RyeWthcgAAA+gAAAPoAAAADS9ob21lL3N0cnlrYXIAAAAAancs
+ ogAAAFAAAABMAAAACXB1YmxpY2tleQAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQ
+ AAACCynm5B6MQrl2iQsUiWVbOWJVlO3qK2HdUdyK75bwMh3QAAARIAAAAdY2hhY2hh
+ MjAtcG9seTEzMDVAb3BlbnNzaC5jb20AAAAdY2hhY2hhMjAtcG9seTEzMDVAb3Blbn
+ NzaC5jb20AAAAAAAAAAAAAAARub25lAAAABG5vbmUAAAAVbWxrZW03Njh4MjU1MTkt
+ c2hhMjU2AAAAAQAAAAtzc2gtZWQyNTUxOQAAADJTSEEyNTY6Y2dRRDJUdjNoMEZsWF
+ ZIK2hBTmh6Si96cVJIUXBWSU9UZXNodzRkQ2x6WQAAAAkxMjcuMC4wLjEAAIUCAAAA
+ CTEyNy4wLjAuMQAACPwAAAAUU1NILTIuMC1PcGVuU1NIXzEwLjQAAAAUU1NILTIuMC
+ 1PcGVuU1NIXzEwLjQAAAAA
+
+Reading the framing: the first eight bytes are the preamble, then
+FORMAT_VERSION 1, then contents as a 446-byte string holding a
+164-byte session block and a 274-byte transport block. Inside the
+session block, methods is 80 bytes holding one 76-byte entry, whose
+own fields are method "publickey", an empty submethod and
+method_info, and a 51-byte key blob. There is no count before the
+entry; the sequence ends when methods is exhausted. In the transport
+block kex_options reads 0x00000001, bit 0 being strict KEX.
+
+7. References
+
+This format follows the conventions of PROTOCOL.sshsig and
+PROTOCOL.krl: an sshbuf wire format with a magic preamble and a uint32
+version, and strict refusal of versions above the maximum supported.
+
+draft-ietf-sshm-cert defines the certificate format carried in
+key_blob when a step presented a certificate. The sk-* key type names
+that can appear in a method entry follow PROTOCOL.u2f. RFC 4251
+section 5 defines the wire encoding primitives used throughout, and
+RFC 4253 section 6.6 the public key serialisation used for key_blob.
diff --git a/auth.h b/auth.h
index 0f11458ca..90c92da9e 100644
--- a/auth.h
+++ b/auth.h
@@ -95,6 +95,7 @@ struct Authctxt {
/* Information exposed to session */
struct sshbuf *session_info; /* Auth info for environment */
+ struct sshbuf *authrec_methods; /* PROTOCOL.authrec method sequence */
};
/*
diff --git a/auth2.c b/auth2.c
index 3f353a719..9fe5f9e9f 100644
--- a/auth2.c
+++ b/auth2.c
@@ -49,6 +49,7 @@
#include "sshkey.h"
#include "hostfile.h"
#include "auth.h"
+#include "authrec.h"
#include "dispatch.h"
#include "pathnames.h"
#include "ssherr.h"
@@ -810,5 +811,8 @@ auth2_update_session_info(Authctxt *authctxt, const char *method,
}
if ((r = sshbuf_put_u8(authctxt->session_info, '\n')) != 0)
fatal_fr(r, "append");
+
+ if ((r = authrec_record_step(authctxt, method, submethod)) != 0)
+ fatal_fr(r, "authrec_record_step");
}
diff --git a/authrec.c b/authrec.c
new file mode 100644
index 000000000..2773c117e
--- /dev/null
+++ b/authrec.c
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2026 Avinash Duduskar
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "includes.h"
+
+#include <sys/types.h>
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "ssherr.h"
+#include "sshbuf.h"
+#include "sshkey.h"
+#include "packet.h"
+#include "kex.h"
+#include "digest.h"
+#include "hostfile.h"
+#include "auth.h"
+#include "authrec.h"
+
+static int
+put_cstring_or_empty(struct sshbuf *b, const char *s)
+{
+ return sshbuf_put_cstring(b, s == NULL ? "" : s);
+}
+
+int
+authrec_record_step(Authctxt *authctxt, const char *method,
+ const char *submethod)
+{
+ struct sshbuf *entry = NULL, *key = NULL;
+ int r;
+
+ if (authctxt->authrec_methods == NULL &&
+ (authctxt->authrec_methods = sshbuf_new()) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+ if ((entry = sshbuf_new()) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+
+ if ((r = put_cstring_or_empty(entry, method)) != 0 ||
+ (r = put_cstring_or_empty(entry, submethod)) != 0 ||
+ (r = put_cstring_or_empty(entry, authctxt->auth_method_info)) != 0)
+ goto out;
+
+ if (authctxt->auth_method_key == NULL) {
+ if ((r = sshbuf_put_string(entry, NULL, 0)) != 0)
+ goto out;
+ } else {
+ if ((key = sshbuf_new()) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if ((r = sshkey_putb(authctxt->auth_method_key, key)) != 0 ||
+ (r = sshbuf_put_stringb(entry, key)) != 0)
+ goto out;
+ }
+
+ /* Each entry carries its own length; the sequence has no count. */
+ r = sshbuf_put_stringb(authctxt->authrec_methods, entry);
+ out:
+ sshbuf_free(key);
+ sshbuf_free(entry);
+ return r;
+}
+
+static int
+session_block(struct ssh *ssh, Authctxt *authctxt, struct sshbuf *b)
+{
+ int r;
+
+ /* The spec requires a resolved passwd entry; refuse to guess. */
+ if (authctxt->pw == NULL)
+ return SSH_ERR_INTERNAL_ERROR;
+
+ if ((r = sshbuf_put_stringb(b, ssh->kex->session_id)) != 0 ||
+ (r = put_cstring_or_empty(b, authctxt->user)) != 0 ||
+ (r = sshbuf_put_u32(b, (u_int)authctxt->pw->pw_uid)) != 0 ||
+ (r = sshbuf_put_u32(b, (u_int)authctxt->pw->pw_gid)) != 0 ||
+ (r = put_cstring_or_empty(b, authctxt->pw->pw_dir)) != 0 ||
+ (r = sshbuf_put_u64(b, (u_int64_t)time(NULL))) != 0)
+ return r;
+
+ if (authctxt->authrec_methods == NULL)
+ return sshbuf_put_string(b, NULL, 0);
+ return sshbuf_put_stringb(b, authctxt->authrec_methods);
+}
+
+static int
+transport_block(struct ssh *ssh, struct sshbuf *b)
+{
+ const char *enc_in = NULL, *mac_in = NULL, *comp_in = NULL;
+ const char *enc_out = NULL, *mac_out = NULL, *comp_out = NULL;
+ struct sshkey *hostkey;
+ char *hostkey_fp = NULL;
+ u_int32_t kex_options = 0;
+ int r;
+
+ /* Server side: MODE_IN is client to server, MODE_OUT the reverse. */
+ if ((r = ssh_packet_get_active_alg_names(ssh, MODE_IN,
+ &enc_in, &mac_in, &comp_in)) != 0 ||
+ (r = ssh_packet_get_active_alg_names(ssh, MODE_OUT,
+ &enc_out, &mac_out, &comp_out)) != 0)
+ return r;
+
+ if (ssh->kex->kex_strict)
+ kex_options |= AUTHREC_KEX_STRICT;
+
+ /* The spec defines no absent case for the fingerprint. */
+ hostkey = get_hostkey_public_by_type(ssh->kex->hostkey_type,
+ ssh->kex->hostkey_nid, ssh);
+ if (hostkey == NULL)
+ return SSH_ERR_INTERNAL_ERROR;
+ if ((hostkey_fp = sshkey_fingerprint(hostkey, SSH_DIGEST_SHA256,
+ SSH_FP_DEFAULT)) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+
+ if ((r = put_cstring_or_empty(b, enc_in)) != 0 ||
+ (r = put_cstring_or_empty(b, enc_out)) != 0 ||
+ (r = put_cstring_or_empty(b, mac_in)) != 0 ||
+ (r = put_cstring_or_empty(b, mac_out)) != 0 ||
+ (r = put_cstring_or_empty(b, comp_in)) != 0 ||
+ (r = put_cstring_or_empty(b, comp_out)) != 0 ||
+ (r = put_cstring_or_empty(b, ssh->kex->name)) != 0 ||
+ (r = sshbuf_put_u32(b, kex_options)) != 0 ||
+ (r = put_cstring_or_empty(b, ssh->kex->hostkey_alg)) != 0 ||
+ (r = put_cstring_or_empty(b, hostkey_fp)) != 0 ||
+ (r = put_cstring_or_empty(b, ssh_remote_ipaddr(ssh))) != 0 ||
+ (r = sshbuf_put_u32(b, (u_int)ssh_remote_port(ssh))) != 0 ||
+ (r = put_cstring_or_empty(b, ssh_local_ipaddr(ssh))) != 0 ||
+ (r = sshbuf_put_u32(b, (u_int)ssh_local_port(ssh))) != 0 ||
+ (r = sshbuf_put_stringb(b, ssh->kex->client_version)) != 0 ||
+ (r = sshbuf_put_stringb(b, ssh->kex->server_version)) != 0 ||
+ (r = put_cstring_or_empty(b, ssh_packet_rdomain_in(ssh))) != 0)
+ goto out;
+ r = 0;
+ out:
+ free(hostkey_fp);
+ return r;
+}
+
+int
+authrec_build(struct ssh *ssh, Authctxt *authctxt, struct sshbuf **out)
+{
+ struct sshbuf *rec = NULL, *contents = NULL;
+ struct sshbuf *session = NULL, *transport = NULL;
+ int r = SSH_ERR_ALLOC_FAIL;
+
+ *out = NULL;
+ if ((rec = sshbuf_new()) == NULL ||
+ (contents = sshbuf_new()) == NULL ||
+ (session = sshbuf_new()) == NULL ||
+ (transport = sshbuf_new()) == NULL)
+ goto out;
+
+ if ((r = session_block(ssh, authctxt, session)) != 0 ||
+ (r = transport_block(ssh, transport)) != 0)
+ goto out;
+
+ if ((r = sshbuf_put_stringb(contents, session)) != 0 ||
+ (r = sshbuf_put_stringb(contents, transport)) != 0)
+ goto out;
+
+ if ((r = sshbuf_put(rec, AUTHREC_MAGIC, AUTHREC_MAGIC_LEN)) != 0 ||
+ (r = sshbuf_put_u32(rec, AUTHREC_FORMAT_V1)) != 0 ||
+ (r = sshbuf_put_stringb(rec, contents)) != 0)
+ goto out;
+
+ *out = rec;
+ rec = NULL;
+ r = 0;
+ out:
+ sshbuf_free(rec);
+ sshbuf_free(contents);
+ sshbuf_free(session);
+ sshbuf_free(transport);
+ return r;
+}
+
+int
+authrec_parse(struct sshbuf *in, u_int *version, struct sshbuf **contents)
+{
+ u_char magic[AUTHREC_MAGIC_LEN];
+ struct sshbuf *body = NULL;
+ u_int ver;
+ int r;
+
+ *version = 0;
+ *contents = NULL;
+
+ if ((r = sshbuf_get(in, magic, sizeof(magic))) != 0 ||
+ (r = sshbuf_get_u32(in, &ver)) != 0)
+ return r;
+ if (memcmp(magic, AUTHREC_MAGIC, AUTHREC_MAGIC_LEN) != 0)
+ return SSH_ERR_INVALID_FORMAT;
+ if (ver == 0 || ver > AUTHREC_FORMAT_V1)
+ return SSH_ERR_INVALID_FORMAT;
+ if ((r = sshbuf_froms(in, &body)) != 0)
+ return r;
+ if (sshbuf_len(in) != 0) {
+ sshbuf_free(body);
+ return SSH_ERR_INVALID_FORMAT;
+ }
+
+ *version = ver;
+ *contents = body;
+ return 0;
+}
diff --git a/authrec.h b/authrec.h
new file mode 100644
index 000000000..38d8f2630
--- /dev/null
+++ b/authrec.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2026 Avinash Duduskar
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* Structured authentication record; see PROTOCOL.authrec */
+
+#ifndef AUTHREC_H
+#define AUTHREC_H
+
+#define AUTHREC_MAGIC "SSHAUTHR"
+#define AUTHREC_MAGIC_LEN 8
+#define AUTHREC_FORMAT_V1 1
+
+/* kex_options bits, transport block */
+#define AUTHREC_KEX_STRICT (1U << 0)
+
+struct ssh;
+struct sshbuf;
+struct Authctxt;
+
+/*
+ * Append one satisfied authentication step to the record's method
+ * sequence. Called alongside the legacy text accumulation in
+ * auth2_update_session_info(), so only steps that succeeded outright
+ * or satisfied a step of AuthenticationMethods are recorded.
+ */
+int authrec_record_step(struct Authctxt *, const char *,
+ const char *);
+
+/*
+ * Build a version-1 record from state available at the site that
+ * writes the legacy ExposeAuthInfo file. On success returns 0 and
+ * sets *out to a freshly allocated sshbuf owned by the caller.
+ */
+int authrec_build(struct ssh *, struct Authctxt *, struct sshbuf **);
+
+/*
+ * Parse the outer framing of a version-1 record. On success returns 0
+ * with *version set and *contents pointing at a fresh sshbuf holding
+ * the blocks. Rejects version 0, versions above AUTHREC_FORMAT_V1,
+ * and data after contents.
+ */
+int authrec_parse(struct sshbuf *, u_int *, struct sshbuf **);
+
+#endif /* AUTHREC_H */
diff --git a/packet.c b/packet.c
index 4c89e60c5..d7d091c28 100644
--- a/packet.c
+++ b/packet.c
@@ -678,6 +678,22 @@ ssh_packet_rdomain_in(struct ssh *ssh)
return ssh->rdomain_in;
}
+/* Active algorithm names for the given mode; see PROTOCOL.authrec */
+int
+ssh_packet_get_active_alg_names(struct ssh *ssh, int mode,
+ const char **enc, const char **mac, const char **comp)
+{
+ struct newkeys *nk;
+
+ if (ssh == NULL || ssh->state == NULL || mode < 0 ||
+ mode >= MODE_MAX || (nk = ssh->state->newkeys[mode]) == NULL)
+ return SSH_ERR_INTERNAL_ERROR;
+ *enc = nk->enc.name;
+ *mac = nk->mac.name;
+ *comp = nk->comp.name;
+ return 0;
+}
+
/* Closes the connection and clears and frees internal data structures. */
static void
@@ -2431,7 +2447,8 @@ kex_to_blob(struct sshbuf *m, struct kex *kex)
(r = sshbuf_put_stringb(m, kex->client_version)) != 0 ||
(r = sshbuf_put_stringb(m, kex->server_version)) != 0 ||
(r = sshbuf_put_stringb(m, kex->session_id)) != 0 ||
- (r = sshbuf_put_u32(m, kex->flags)) != 0)
+ (r = sshbuf_put_u32(m, kex->flags)) != 0 ||
+ (r = sshbuf_put_cstring(m, kex->name == NULL ? "" : kex->name)) != 0)
return r;
return 0;
}
@@ -2605,7 +2622,8 @@ kex_from_blob(struct sshbuf *m, struct kex **kexp)
(r = sshbuf_get_stringb(m, kex->client_version)) != 0 ||
(r = sshbuf_get_stringb(m, kex->server_version)) != 0 ||
(r = sshbuf_get_stringb(m, kex->session_id)) != 0 ||
- (r = sshbuf_get_u32(m, &kex->flags)) != 0)
+ (r = sshbuf_get_u32(m, &kex->flags)) != 0 ||
+ (r = sshbuf_get_cstring(m, &kex->name, NULL)) != 0)
goto out;
if (kex->we_need > 1024) {
r = SSH_ERR_INVALID_FORMAT;
diff --git a/packet.h b/packet.h
index 3e8acb2cd..980d9ca11 100644
--- a/packet.h
+++ b/packet.h
@@ -166,6 +166,8 @@ int ssh_remote_port(struct ssh *);
const char *ssh_local_ipaddr(struct ssh *);
int ssh_local_port(struct ssh *);
const char *ssh_packet_rdomain_in(struct ssh *);
+int ssh_packet_get_active_alg_names(struct ssh *, int,
+ const char **, const char **, const char **);
char *ssh_remote_hostname(struct ssh *);
void ssh_packet_set_rekey_limits(struct ssh *, uint64_t, uint32_t);
diff --git a/regress/Makefile b/regress/Makefile
index a6f81bef4..ef8c6a724 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -301,6 +301,7 @@ unit unit-bench:
-d ${.CURDIR}/unittests/sshsig/testdata $${ARGS}; \
$$V ${.OBJDIR}/unittests/authopt/test_authopt \
-d ${.CURDIR}/unittests/authopt/testdata $${ARGS}; \
+ $$V ${.OBJDIR}/unittests/authrec/test_authrec $${ARGS}; \
$$V ${.OBJDIR}/unittests/bitmap/test_bitmap $${ARGS}; \
$$V ${.OBJDIR}/unittests/conversion/test_conversion $${ARGS}; \
$$V ${.OBJDIR}/unittests/kex/test_kex $${ARGS}; \
diff --git a/regress/unittests/Makefile b/regress/unittests/Makefile
index 5d482eab4..f30f85214 100644
--- a/regress/unittests/Makefile
+++ b/regress/unittests/Makefile
@@ -1,6 +1,6 @@
# $OpenBSD: Makefile,v 1.15 2026/06/14 04:08:05 djm Exp $
SUBDIR= test_helper sshbuf sshkey bitmap kex hostkeys utf8 match conversion
-SUBDIR+=authopt misc sshsig servconf crypto
+SUBDIR+=authopt authrec misc sshsig servconf crypto
.include <bsd.subdir.mk>
diff --git a/regress/unittests/authrec/Makefile b/regress/unittests/authrec/Makefile
new file mode 100644
index 000000000..45231ad7d
--- /dev/null
+++ b/regress/unittests/authrec/Makefile
@@ -0,0 +1,44 @@
+# $OpenBSD$
+
+PROG=test_authrec
+SRCS=tests.c
+
+SRCS+=authrec.c
+
+# From usr.bin/ssh; authrec.c pulls packet.c, and packet.c the rest.
+SRCS+=sshbuf-getput-basic.c sshbuf-getput-crypto.c sshbuf-misc.c sshbuf.c
+SRCS+=sshbuf-io.c atomicio.c sshkey.c authfile.c cipher.c log.c ssh-rsa.c
+SRCS+=ssh-ecdsa.c ssh-ed25519.c mac.c umac.c umac128.c hmac.c misc.c
+SRCS+=ssherr.c uidswap.c cleanup.c xmalloc.c match.c krl.c fatal.c
+SRCS+=addr.c addrmatch.c bitmap.c packet.c dispatch.c canohost.c ssh_api.c
+SRCS+=compat.c
+SRCS+=cipher-chachapoly.c chacha.c poly1305.c ssh-ecdsa-sk.c ssh-sk.c
+SRCS+=ssh-ed25519-sk.c sk-usbhid.c ssh-pkcs11-client.c ssherr-libcrypto.c
+SRCS+=libcrux-mlkem-mldsa.c ssh-mldsa-eddsa.c
+SRCS+= kex.c
+SRCS+= kex-names.c
+SRCS+= dh.c
+SRCS+= kexdh.c
+SRCS+= kexecdh.c
+SRCS+= kexgex.c
+SRCS+= kexgexc.c
+SRCS+= kexgexs.c
+SRCS+= kexc25519.c
+SRCS+= smult_curve25519_ref.c
+SRCS+= kexgen.c
+SRCS+= kexsntrup761x25519.c
+SRCS+= kexmlkem768x25519.c
+SRCS+= sntrup761.c
+SRCS+= utf8.c
+
+SRCS+=digest-openssl.c ed25519-openssl.c
+#SRCS+=digest-libc.c ed25519.c
+
+REGRESS_TARGETS=run-regress-${PROG}
+
+run-regress-${PROG}: ${PROG}
+ env ${TEST_ENV} ./${PROG} ${UNITTEST_ARGS}
+
+.include <bsd.regress.mk>
+
+LDADD+=-lz
diff --git a/regress/unittests/authrec/tests.c b/regress/unittests/authrec/tests.c
new file mode 100644
index 000000000..a74bac211
--- /dev/null
+++ b/regress/unittests/authrec/tests.c
@@ -0,0 +1,123 @@
+/*
+ * Regress test for the PROTOCOL.authrec outer framing parser.
+ *
+ * Placed in the public domain
+ */
+
+#include "includes.h"
+
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../test_helper/test_helper.h"
+
+#include "ssherr.h"
+#include "sshbuf.h"
+#include "authrec.h"
+
+/* authrec.o links server-only symbols; the parser never calls this. */
+struct ssh;
+struct sshkey *get_hostkey_public_by_type(int, int, struct ssh *);
+struct sshkey *
+get_hostkey_public_by_type(int type, int nid, struct ssh *ssh)
+{
+ return NULL;
+}
+
+static struct sshbuf *
+build_record(u_int version, const u_char *magic)
+{
+ struct sshbuf *rec, *contents;
+
+ rec = sshbuf_new();
+ ASSERT_PTR_NE(rec, NULL);
+ contents = sshbuf_new();
+ ASSERT_PTR_NE(contents, NULL);
+ /* two enclosed blocks, contents opaque to the outer parser */
+ ASSERT_INT_EQ(sshbuf_put_cstring(contents, "session"), 0);
+ ASSERT_INT_EQ(sshbuf_put_cstring(contents, "transport"), 0);
+ ASSERT_INT_EQ(sshbuf_put(rec, magic, AUTHREC_MAGIC_LEN), 0);
+ ASSERT_INT_EQ(sshbuf_put_u32(rec, version), 0);
+ ASSERT_INT_EQ(sshbuf_put_stringb(rec, contents), 0);
+ sshbuf_free(contents);
+ return rec;
+}
+
+void
+tests(void)
+{
+ struct sshbuf *rec, *cut, *contents;
+ u_int version;
+ size_t i, len;
+ int r;
+
+ TEST_START("parse valid record");
+ rec = build_record(AUTHREC_FORMAT_V1, AUTHREC_MAGIC);
+ contents = NULL;
+ version = 0;
+ ASSERT_INT_EQ(authrec_parse(rec, &version, &contents), 0);
+ ASSERT_U_INT_EQ(version, AUTHREC_FORMAT_V1);
+ ASSERT_PTR_NE(contents, NULL);
+ ASSERT_SIZE_T_EQ(sshbuf_len(rec), 0);
+ sshbuf_free(contents);
+ sshbuf_free(rec);
+ TEST_DONE();
+
+ TEST_START("reject version 0");
+ rec = build_record(0, AUTHREC_MAGIC);
+ r = authrec_parse(rec, &version, &contents);
+ ASSERT_INT_EQ(r, SSH_ERR_INVALID_FORMAT);
+ ASSERT_PTR_EQ(contents, NULL);
+ sshbuf_free(rec);
+ TEST_DONE();
+
+ TEST_START("reject version above maximum");
+ rec = build_record(AUTHREC_FORMAT_V1 + 1, AUTHREC_MAGIC);
+ r = authrec_parse(rec, &version, &contents);
+ ASSERT_INT_EQ(r, SSH_ERR_INVALID_FORMAT);
+ ASSERT_PTR_EQ(contents, NULL);
+ sshbuf_free(rec);
+ TEST_DONE();
+
+ TEST_START("reject bad magic");
+ rec = build_record(AUTHREC_FORMAT_V1, "SSHAUTHX");
+ r = authrec_parse(rec, &version, &contents);
+ ASSERT_INT_EQ(r, SSH_ERR_INVALID_FORMAT);
+ ASSERT_PTR_EQ(contents, NULL);
+ sshbuf_free(rec);
+ TEST_DONE();
+
+ TEST_START("reject every truncation");
+ rec = build_record(AUTHREC_FORMAT_V1, AUTHREC_MAGIC);
+ len = sshbuf_len(rec);
+ for (i = 0; i < len; i++) {
+ cut = sshbuf_new();
+ ASSERT_PTR_NE(cut, NULL);
+ ASSERT_INT_EQ(sshbuf_put(cut, sshbuf_ptr(rec), i), 0);
+ test_subtest_info("truncated to %zu of %zu", i, len);
+ r = authrec_parse(cut, &version, &contents);
+ ASSERT_INT_NE(r, 0);
+ ASSERT_PTR_EQ(contents, NULL);
+ sshbuf_free(cut);
+ }
+ sshbuf_free(rec);
+ TEST_DONE();
+
+ TEST_START("reject trailing data after contents");
+ rec = build_record(AUTHREC_FORMAT_V1, AUTHREC_MAGIC);
+ ASSERT_INT_EQ(sshbuf_put_u8(rec, 0), 0);
+ r = authrec_parse(rec, &version, &contents);
+ ASSERT_INT_EQ(r, SSH_ERR_INVALID_FORMAT);
+ ASSERT_PTR_EQ(contents, NULL);
+ sshbuf_free(rec);
+ TEST_DONE();
+}
+
+void
+benchmarks(void)
+{
+ printf("no benchmarks\n");
+}
diff --git a/servconf.c b/servconf.c
index 9b443bea0..1e0d3e53f 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1084,6 +1084,12 @@ static const struct multistate multistate_gatewayports[] = {
{ "no", 0 },
{ NULL, -1 }
};
+static const struct multistate multistate_exposeauthinfo[] = {
+ { "yes", EXPOSE_AUTHINFO_YES },
+ { "structured", EXPOSE_AUTHINFO_STRUCTURED },
+ { "no", EXPOSE_AUTHINFO_NO },
+ { NULL, -1 }
+};
static const struct multistate multistate_tcpfwd[] = {
{ "yes", FORWARD_ALLOW },
{ "all", FORWARD_ALLOW },
@@ -2475,7 +2481,8 @@ process_server_config_line_depth(ServerOptions *options, char *line,
case sExposeAuthInfo:
intptr = &options->expose_userauth_info;
- goto parse_flag;
+ multistate_ptr = multistate_exposeauthinfo;
+ goto parse_multistate;
case sRDomain:
#if !defined(__OpenBSD__) && !defined(HAVE_SYS_SET_PROCESS_RDOMAIN)
@@ -4044,6 +4051,8 @@ fmt_intarg(ServerOpCodes code, int val)
return fmt_multistate_int(val, multistate_gatewayports);
case sCompression:
return fmt_multistate_int(val, multistate_compression);
+ case sExposeAuthInfo:
+ return fmt_multistate_int(val, multistate_exposeauthinfo);
case sAllowTcpForwarding:
return fmt_multistate_int(val, multistate_tcpfwd);
case sAllowStreamLocalForwarding:
diff --git a/servconf.h b/servconf.h
index a2345e88a..6fc3410b2 100644
--- a/servconf.h
+++ b/servconf.h
@@ -38,6 +38,10 @@ struct sshbuf;
#define IGNORE_RHOSTS_YES 1
#define IGNORE_RHOSTS_SHOSTS 2
+#define EXPOSE_AUTHINFO_NO 0
+#define EXPOSE_AUTHINFO_YES 1
+#define EXPOSE_AUTHINFO_STRUCTURED 2
+
#define DEFAULT_AUTH_FAIL_MAX 6 /* Default for MaxAuthTries */
#define DEFAULT_SESSIONS_MAX 10 /* Default for MaxSessions */
@@ -228,7 +232,7 @@ SSHCONF_STRING(authorized_principals_command_user, AuthorizedPrincipalsCommandUs
SSHCONF_STRING(version_addendum, VersionAddendum, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_STRARRAY(auth_methods, num_auth_methods, AuthenticationMethods, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
SSHCONF_INT(fingerprint_hash, FingerprintHash, SSHCFG_GLOBAL, NULL, SSH_FP_HASH_DEFAULT, SSHCFG_COPY_NONE) \
-SSHCONF_INTFLAG(expose_userauth_info, ExposeAuthInfo, SSHCFG_ALL, 0, SSHCFG_COPY_MATCH) \
+SSHCONF_INT(expose_userauth_info, ExposeAuthInfo, SSHCFG_ALL, multistate_exposeauthinfo, 0, SSHCFG_COPY_MATCH) \
SSHCONF_STRING(sk_provider, SecurityKeyProvider, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
SSHCONF_INT(required_rsa_size, RequiredRSASize, SSHCFG_ALL, NULL, SSH_RSA_MINIMUM_MODULUS_SIZE, SSHCFG_COPY_MATCH) \
SSHCONF_STRARRAY(channel_timeouts, num_channel_timeouts, ChannelTimeout, SSHCFG_ALL, SSHCFG_COPY_MATCH) \
diff --git a/session.c b/session.c
index fc9c9d6f8..b48c2e6a6 100644
--- a/session.c
+++ b/session.c
@@ -74,6 +74,7 @@
#include "kex.h"
#include "hostfile.h"
#include "auth.h"
+#include "authrec.h"
#include "auth-options.h"
#include "authfd.h"
#include "pathnames.h"
@@ -236,11 +237,23 @@ display_loginmsg(void)
}
static void
-prepare_auth_info_file(struct passwd *pw, struct sshbuf *info)
+prepare_auth_info_file(struct ssh *ssh, Authctxt *authctxt)
{
- int fd = -1, success = 0;
+ struct passwd *pw = authctxt->pw;
+ struct sshbuf *info = authctxt->session_info;
+ struct sshbuf *rec = NULL;
+ int fd = -1, success = 0, r;
- if (!options.expose_userauth_info || info == NULL)
+ if (options.expose_userauth_info == EXPOSE_AUTHINFO_NO)
+ return;
+ if (options.expose_userauth_info == EXPOSE_AUTHINFO_STRUCTURED) {
+ if ((r = authrec_build(ssh, authctxt, &rec)) != 0) {
+ error_fr(r, "authrec_build");
+ return;
+ }
+ info = rec;
+ }
+ if (info == NULL)
return;
temporarily_use_uid(pw);
@@ -260,6 +273,7 @@ prepare_auth_info_file(struct passwd *pw, struct sshbuf *info)
}
success = 1;
out:
+ sshbuf_free(rec);
if (!success) {
if (fd != -1)
close(fd);
@@ -335,7 +349,7 @@ do_authenticated(struct ssh *ssh, Authctxt *authctxt)
}
auth_debug_send(ssh);
- prepare_auth_info_file(authctxt->pw, authctxt->session_info);
+ prepare_auth_info_file(ssh, authctxt);
do_authenticated2(ssh, authctxt);
diff --git a/sshd_config.5 b/sshd_config.5
index 39a864dee..5c808fd54 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -690,6 +690,12 @@ public credentials (e.g. keys) used to authenticate the user.
The location of the file is exposed to the user session through the
.Ev SSH_USER_AUTH
environment variable.
+If set to
+.Cm structured ,
+the file instead contains a binary record of the completed
+authentication, described in the
+.Pa PROTOCOL.authrec
+file in the source distribution.
The default is
.Cm no .
.It Cm FingerprintHash
--
2.55.0
More information about the openssh-unix-dev
mailing list