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