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