RFC: PROTOCOL.authrec, a structured authentication record from sshd
Avinash Duduskar
avinash.duduskar at gmail.com
Tue Jul 14 06:01:58 AEST 2026
Hi,
By the end of userauth, sshd holds a set of structured facts:
session identity, transport parameters, the authentication method
sequence, the keys and certificates that authenticated.
ExposeAuthInfo=yes (shipped in 7.6) exports a line-based text file
carrying a fraction of them. Consumers that need the rest (audit
pipelines, session policy hooks, identity-provider liveness checks)
reconstruct it from log scraping or by parsing key blobs out of the
text file.
The proposal is a canonical emission format, documented as
PROTOCOL.authrec:
- an sshbuf-encoded, length-bounded, versioned record (magic preamble
plus format version, the PROTOCOL.sshsig and PROTOCOL.krl convention),
produced at the same write site the ExposeAuthInfo file already uses
- version 1 is the record plus one delivery channel, a new
ExposeAuthInfo=structured value writing it to the existing
$SSH_USER_AUTH path
- the record is a structured superset of the ExposeAuthInfo text,
including the per-step method sequence with each step's key
- the =yes behaviour is unchanged
Deliberately out of version 1, each a possible follow-on:
- a structured PAM channel (SSH_AUTH_INFO_0 already exports the
text to PAM, but it is a C-string channel and the record
carries NUL bytes it cannot)
- a post-auth command gate (AuthorizedKeysCommand-shaped: record
on stdin, exit code decides the session)
- a partial record for AuthorizedKeysCommand and
AuthorizedPrincipalsCommand
- further delivery channels (audit syslog adjuncts, sshd status
queries); the wire format is delivery-agnostic
- the sk-* runtime fields (locals freed when userauth_pubkey()
returns, before the write site)
- per-step attribution of authorized_keys options (only the merged
active set survives to the write site)
- an attestation block (the extension rules reserve a type-tagged
wrapper for it, but no mechanism exists upstream to populate it
yet)
- a JSON rendering tool (the wire format is the canonical artefact)
I found no prior proposal for a structured form of this export in
the openssh-unix-dev archives. Damien saw an earlier draft of this
off-list and suggested posting it here.
A prototype of the marshaller and the file channel produced the
example in section 6 from a live authentication. If the shape holds
up here, the implementation follows as a 2-patch series: the
marshaller and a one-string extension of the auth-to-session state
transfer, with a roundtrip regress test, then the
ExposeAuthInfo=structured keyword value.
The spec:
This document describes a structured authentication-record format
produced by sshd at userauth completion. The format is intended for
consumption by site-specific tooling (audit pipelines, session-policy
decision hooks, identity-provider liveness checks) that needs the
full set of facts sshd determined during userauth, in a stable,
machine-readable, version-compatible form.
The format is sshbuf-encoded, length-bounded, versioned, and
extensible. It is the canonical artefact. Tooling that prefers JSON
or other text formats consumes a rendering operation (see section 5)
and need not parse sshbuf directly.
Version 1 defines the record and one delivery channel: an extension
to the existing ExposeAuthInfo file mechanism shipped in OpenSSH 7.6
(section 3.1). The wire format is delivery-agnostic; additional
channels may be added later without changes to the grammar
(section 3.2).
1. Wire format
#define MAGIC_PREAMBLE "SSHAUTHR"
#define FORMAT_VERSION 0x00000001
byte[8] MAGIC_PREAMBLE
uint32 FORMAT_VERSION
uint32 record_length /* total bytes following this
field, see section 2 */
string session_block
string transport_block
string credential_block
string cert_block
string reserved /* empty in version 1; section 2 */
Each block is an sshbuf-encoded sub-record carried as an SSH
string (uint32 length followed by length bytes of content).
Blocks that do not apply to the authentication event (for
instance, the certificate block when pubkey-without-cert auth
was used) are present but empty (zero-length string). Parsers
MUST tolerate empty blocks. Each block also carries a trailing
reserved field; all reserved fields are empty in version 1
(section 2). Producers of a version-1 record MUST emit every
version-1 field and block, using an empty value where the field
or block does not apply.
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. FORMAT_VERSION starts at 1; record_length
is the extensibility cushion. See section 2 for the versioning and
extensibility rules and their interaction.
1.1 Session block
string session_id /* canonical SSH session_id from
the first key exchange */
string user /* target username */
uint32 uid /* numeric user ID */
uint32 gid /* numeric primary group ID */
string home /* user's home directory */
uint64 auth_time /* seconds since Unix epoch at
userauth completion */
string methods /* the authentication method
sequence; sub-sshbuf, below */
string reserved /* empty in version 1; section 2 */
methods is an sshbuf carrying one entry per authentication step,
in the order the steps were satisfied:
uint32 count
repeat count times:
string method /* "publickey", "password",
"hostbased",
"keyboard-interactive",
"gssapi-with-mic" */
string submethod /* method-specific detail or empty */
string method_info /* free-form per-method info or
empty */
string key_blob /* the step's public key in the
standard SSH serialisation, or
the full certificate for a
certificate key, for key-bearing
methods (publickey, hostbased);
empty otherwise. The same key the
legacy file appends as text */
For single-method authentication count is 1. For multi-step
authentication configured via AuthenticationMethods (for example
"publickey,keyboard-interactive"), methods carries one entry per
step, in the order satisfied, mirroring the per-method lines the
legacy ExposeAuthInfo file accumulates. A record that captured only
the final method would be lossy relative to that file; the sequence
is what keeps the structured format a superset of the text it
replaces.
submethod is empty for every method except keyboard-interactive,
where it carries the kbd-int device name that satisfied the
challenge. Concrete device names in current OpenSSH are "bsdauth"
and "pam"; populators for new kbd-int devices MUST set submethod
to the device name used.
method_info mirrors the free-form per-method info field that
ExposeAuthInfo=yes captures today: the FIDO signature counter for
sk-* publickey auth, the client user and host for hostbased auth,
and the client principal's display name for GSSAPI auth. Methods
that record no such text (password, keyboard-interactive in
current OpenSSH) leave it empty.
key_blob carries the per-step key, so the methods block records which
key satisfied which step. This is what keeps the format a superset of
the legacy file, which appends the same per-step key as text. Without
it, multi-step authentication where a key-bearing method is not the
last step (for example "publickey,password", or "publickey,publickey"
with distinct keys) would drop a key the text file keeps; with it the
methods block carries them all. For a certificate step the serialised
key is the full certificate, so each step's certificate is preserved
here even when it is not the last key-bearing step.
The Credential block (section 1.3) is a top-level convenience view
of the key for a single key-bearing step, not the sole carrier of
key identity: per-step key identity for a chain lives in the
methods block above. For single-method auth the Credential block
is that step's key; for multi-step auth it is the last key-bearing
step's, so it is not empty whenever some step presented a key.
1.2 Transport block
string cipher_c2s /* client-to-server cipher name */
string cipher_s2c /* server-to-client cipher name */
string mac_c2s /* MAC name; empty for AEAD ciphers,
which authenticate internally
(see cipher_c2s) */
string mac_s2c /* MAC name; empty for AEAD ciphers,
which authenticate internally
(see cipher_s2c) */
string comp_c2s /* compression method, "none" if disabled */
string comp_s2c /* compression method, "none" if disabled */
string kex_algorithm /* negotiated KEX algorithm */
byte strict_kex /* 1 if strict KEX
(kex-strict-*-v00 at openssh.com, the
Terrapin mitigation) was
negotiated, else 0 */
string hostkey_algorithm /* negotiated host-key algorithm */
string hostkey_fingerprint /* SHA-256 fingerprint of the
host key sshd presented, in
OpenSSH "SHA256:..." form */
string client_addr /* client IP address as text */
uint32 client_port /* client port */
string server_addr /* server IP address as text */
uint32 server_port /* server port */
string client_version /* client identification string,
the V_C input to the KEX hash
("SSH-2.0-..." without CRLF) */
string server_version /* server identification string,
the V_S input to the KEX hash
("SSH-2.0-..." without CRLF) */
string rdomain /* routing domain the connection
arrived on, as exposed to
AuthorizedKeysCommand via %D;
empty if none */
string reserved /* empty in version 1; section 2 */
hostkey_fingerprint identifies the specific host key sshd
presented to the client. In CA-signed host-key deployments with
multiple keys in rotation, this field lets audit consumers
distinguish which key was in use without re-parsing the wire
transcript.
1.3 Credential block (publickey and hostbased auth only)
string key_type /* e.g. "sk-ssh-ed25519 at openssh.com" */
string key_fingerprint /* SHA-256 fingerprint in OpenSSH
"SHA256:..." form */
string key_blob /* RFC 4253 serialised public key;
the full certificate blob when the
key is a certificate */
string reserved /* empty in version 1; section 2 */
This block holds the key identity for any authentication that
presented a key: the user key for publickey, the client host key for
hostbased. For multi-step auth it is the last key-bearing step's key,
so when the last key-bearing step is hostbased it carries the client
host key, and when it is publickey it carries the user key. The full
per-step chain, including the key of any earlier key-bearing step, is
in the methods block (section 1.1). The block is empty only when no
step presented a key.
The runtime FIDO fields (signature counter and flags) and the
signature algorithm used for the publickey signature are not
carried in version 1; see section 4.1.
1.4 Certificate block (empty for non-cert auth)
string ca_fingerprint /* SHA-256 fingerprint of the CA
key */
uint64 cert_serial /* certificate serial, matching
the uint64 serial field of
the SSH certificate format
per draft-ietf-sshm-cert */
string cert_key_id /* OpenSSH "key id"; the "identifier"
field of draft-ietf-sshm-cert */
uint64 cert_valid_after /* not-before, Unix epoch */
uint64 cert_valid_before /* not-after, Unix epoch */
string principal_list
string reserved /* empty in version 1; section 2 */
principal_list is an sshbuf with:
uint32 count
repeat count times:
string principal
These are the identity-bearing fields of the certificate that
authorised the matched key. For multi-step auth with more than one
certificate in the chain, the fields reflect the last key-bearing
step's certificate; each step's full certificate is preserved in
that step's key_blob in the methods block (section 1.1). The
certificate's critical options and extensions are not carried as
structured maps in version 1; see section 4.1.
2. Versioning and extensibility
FORMAT_VERSION starts at 1. It is incremented only on changes
that are not backward-compatible: repurposing or removing a
field, repurposing or removing a block, changing the meaning of
an existing enumerated value, or any change that would cause an
older parser to mis-interpret a newer record. Parsers MUST refuse
records with FORMAT_VERSION greater than the maximum they support.
record_length is the extensibility cushion. It records the total
byte count following the record_length field itself: all block
strings and the top-level reserved field, each including its
length prefix. Producers MUST write record_length accurately.
Consumers MUST use record_length to bound the top-level read.
This is what "length-bounded" means throughout this document:
every read is bounded by an explicit length, record_length at the
top level and the string length prefixes within it. Consumers MAY
refuse records whose record_length exceeds an implementation
limit.
The reserved fields sit at fixed positions: each block's reserved
field immediately follows that block's version-1 fields, and the
top-level reserved field immediately follows the version-1 blocks.
Additive growth happens after a reserved field, never before it,
so a reserved field's position does not move as the format grows.
None of the following bumps FORMAT_VERSION:
- Adding fields to an existing block: append them within the
block's length-prefix, after that block's reserved field.
- Adding a new block: append it after the top-level reserved
field, within record_length.
- Placing a small addition in a reserved field itself: a later
revision of this format MAY make a reserved field non-empty.
Old parsers read the fields and blocks they know, then stop:
trailing bytes within a block (bounded by its length prefix) and
trailing blocks (bounded by record_length) are skipped without
interpretation.
New parsers, reading an older record, MUST treat an exhausted buffer
as "field absent" or "block absent": before each read past the
version-1 content they check that the enclosing length prefix (for a
field) or record_length (for a block) is not exhausted, and they MUST
NOT error when it is. A non-empty reserved field MUST likewise be
accepted and ignored by a parser that does not understand it, rather
than treated as an error.
The reserved field follows PROTOCOL.sshsig's convention: a
fixed-position slot, empty in version 1, that a parser which does
not understand it ignores rather than rejects. The
record_length-bounded trailing append for new fields and blocks is
this format's own mechanism. The sshsig format has no outer length
field, and its verifier rejects trailing bytes (sshsig.c), so the
append tolerance here is a deliberate divergence from it, not a
borrowing. Neither mechanism bumps FORMAT_VERSION.
3. Delivery channels
3.1 File: ExposeAuthInfo=structured
A new value for the sshd_config(5) ExposeAuthInfo keyword.
When set, sshd writes the authentication record to the same
file path exported via the $SSH_USER_AUTH environment variable
(the existing ExposeAuthInfo lifecycle, shipped in OpenSSH
7.6). The existing ExposeAuthInfo=yes behaviour is unchanged
and continues to emit the legacy text format. Operators
choosing the structured mode receive the wire format described
in this document at the same file path.
3.2 Additional channels
Channels beyond the file may be added later without changes to the
record grammar. Each is an independent proposal; the record format
stands on its own. Candidates identified so far, all deferred:
- a PAM channel, exporting the record to PAM modules the way
ExposeAuthInfo=yes exports the text today. This needs a
binary-safe encoding, because the record contains NUL bytes
that the existing C-string environment channel cannot carry,
and its own emission point at userauth completion.
- a post-auth subprocess gate whose exit code decides session
establishment.
- a partial record supplied to AuthorizedKeysCommand and
AuthorizedPrincipalsCommand, which run during userauth before
the record is complete: the same grammar with only the blocks
known at that point populated.
4. Population
The record is assembled after authentication completes, at the
same site that writes the legacy ExposeAuthInfo file. Every field
is drawn from state available at that site; producing the record
changes no authentication behaviour.
The method sequence is accumulated one entry per completed step,
in step order, alongside the legacy text accumulation. The
Credential block carries the last key-bearing step's key, and the
Certificate block that same step's certificate; both are empty
when no step presented a key.
4.1 Fields and blocks deferred from version 1
Version 1 is scoped to facts the session child can obtain without
retaining values that an auth method frees on return. Several useful
items are deferred to follow-on work, either because reaching them
would require that retention, or because no mechanism to produce
them exists upstream yet. Each is additive (section 2) and can be
introduced without bumping FORMAT_VERSION.
- Runtime publickey fields: the signature algorithm actually used
(rsa-sha2-512 vs rsa-sha2-256 vs legacy SHA-1 ssh-rsa, distinct
from key_type) and the sk-* FIDO runtime fields (signature counter
and flags). sshd does not retain these to the emission point today;
carrying them is a retention change rather than emission of an
existing value, deferred so version 1 reaches into no auth method.
The legacy ExposeAuthInfo file carries neither the algorithm nor
the flags, and the sk signature counter it does carry survives in
the methods block's method_info, so deferring these does not make
the record a non-superset of that file.
- The authorized_keys options block (which authorized_keys options
sshd parsed and applied for the matched key): only the merged,
possibly session-restricted active set survives to the emission
point, which is not the faithful per-key map. Deferred until the
same retention change carries the per-key options. The legacy file
does not carry these options.
- Certificate critical options and extensions as structured maps: the
full certificate blob is already carried in key_blob, so structured
maps of its option fields are a parsing convenience, not new
information. They can be added later per section 2; nothing is lost
meanwhile, since the fields are recoverable from the carried blob.
- The attestation block: FIDO attestation is captured at enrollment,
not at runtime userauth, so it has no on-the-wire source, and no
mechanism to supply it exists upstream yet (no AAGUID parser, no
server-local attestation sidecar format, no certificate critical-
option vocabulary for it). A follow-on that adds such a mechanism
introduces the block as a type-tagged wrapper (an attestation_type
string plus a type-specific attestation_data sub-record), so a
parser that does not recognise a type treats the block as empty and
future types need no FORMAT_VERSION bump.
- signature-key authorization provenance (which authorized_keys
file/line, vs AuthorizedKeysCommand, vs a trusted CA), the
matched principal under AuthorizedPrincipalsFile/Command,
failed-attempt count, authentication style, service name, and
Kerberos/GSSAPI ticket detail: per-session history, facts sshd
computes during a method but does not retain to the write site,
or method-niche state, all outside an auth-completion snapshot.
5. Operator interface
The wire format is sshbuf-encoded for OpenSSH-internal handling. A
rendering operation that projects a record to JSON, for tooling that
does not consume sshbuf natively, is anticipated as a follow-on. The
JSON projection is a property of that tool, not of the wire format,
and ships with the tool rather than being defined here; alternative
renderings (CBOR, key=value text) could be added the same way without
affecting the wire format.
6. Examples
The record below was produced by a prototype of the marshaller
from a live loopback authentication (single publickey step,
ed25519 user key, no certificate, mlkem768x25519-sha256 KEX,
chacha20-poly1305 with no separate MAC, compression "none"). It
is 601 bytes; the certificate block and all reserved fields are
empty.
U1NIQVVUSFIAAAABAAACSQAAAKIAAAAgeDspJAnIGpTqhmCr9ZAizKzR0rG1Rt3A
xZ3XmpoLR7wAAAAEZGVtbwAAA+gAAAPoAAAACi9ob21lL2RlbW8AAAAAalU0WQAA
AFAAAAABAAAACXB1YmxpY2tleQAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAA
ACBmEhDaL+VpW/VWUdWmsxB6GHa4t8fW1jsQFRoHJqBkqAAAAAAAAAETAAAAHWNo
YWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAAHWNoYWNoYTIwLXBvbHkxMzA1
QG9wZW5zc2guY29tAAAAAAAAAAAAAAAEbm9uZQAAAARub25lAAAAFW1sa2VtNzY4
eDI1NTE5LXNoYTI1NgEAAAALc3NoLWVkMjU1MTkAAAAyU0hBMjU2OmNwWmFuRXRC
ay9KNUtrK1JWc2FsOVJlWTFBMlZBR3BjRTQyMEVzWE01Rk0AAAAJMTI3LjAuMC4x
AADF3AAAAAkxMjcuMC4wLjEAAAj7AAAAFFNTSC0yLjAtT3BlblNTSF8xMC40AAAA
FFNTSC0yLjAtT3BlblNTSF8xMC40AAAAAAAAAAAAAACAAAAAC3NzaC1lZDI1NTE5
AAAAMlNIQTI1NjpsQzVheWRBZXpkNGdmK08vU1ZBZjVOaC8wVkhwaGp5YktOMGxx
K24vdVpJAAAAMwAAAAtzc2gtZWQyNTUxOQAAACBmEhDaL+VpW/VWUdWmsxB6GHa4
t8fW1jsQFRoHJqBkqAAAAAAAAAAAAAAAAA==
7. References
This format follows the encoding conventions established by
PROTOCOL.sshsig and PROTOCOL.krl: the sshbuf wire format with a
magic preamble and a uint32 format version, strict refusal of
versions above the maximum supported, and (from sshsig) a
reserved field that is empty in version 1 and ignored if
non-empty.
draft-ietf-sshm-cert defines the certificate format whose parsed
fields populate section 1.4. The sk-* key type names that can
appear in the credential block follow PROTOCOL.u2f.
RFC 4251 (section 5) defines the SSH wire encoding primitives used
throughout: byte, byte[n], uint32, uint64, and string. RFC 4253
(section 6.6) defines the public key blob serialisation used for
key_blob.
Thanks,
Avi
More information about the openssh-unix-dev
mailing list