Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

Peios Generic System Standards

The cross-platform standards a system must implement to be Peios — contracts between two roles, either of which a third party may replace.

Single-page view · as markdown

1.1 Scope

Peios / Peios technical reference / PGSS / Introduction

This document defines the Peios Generic System Standards (PGSS): the cross-platform protocols and standards a system MUST implement in order to be Peios.

A standard belongs in this document when three things are true of it.

It is a conformance requirement. A system that does not offer the protocol, at the specified path, with the specified semantics, is not Peios. Each standard here is a bar to clear, not a recommendation to weigh.

It belongs to no implementation. A PGSS standard describes a contract between two roles, not the behaviour of a particular program. Mainline ships an implementation of each role; nothing in this document depends on that implementation, or describes it.

Either role may be replaced. A third party MAY ship its own implementation of one role — or of both — and interoperate with the other unchanged. A standard that cannot survive that substitution is a description of one system rather than a contract between two, and does not belong here.

For each standard, this document covers:

  • the channel it is offered on, and the access control governing it
  • message framing, encoding, and the rules under which the format may be extended
  • the messages exchanged, their fields, and the order in which they are exchanged
  • the obligations binding on each role, including what a role MUST establish for itself rather than believe from a message
  • the conformance requirements for each role

This document does not cover:

  • How an implementation reaches the answers it gives — that is precisely what different systems exist to do differently
  • The binary structures these protocols carry — defined in PCDS
  • Protocols spoken across the kernel boundary — defined in PSPK
  • Protocols between foundational userspace components — defined in PSPU
  • Interfaces particular to one Mainline component — defined in the specification of the component that offers them

1.1.1 Distinguishing a standard from a protocol #

The anthology's three protocol documents are told apart by what happens when you disagree with one.

Disagreeing with a standard in this document means shipping something that is not Peios.

Disagreeing with PSPK or PSPU means shipping a system built from different parts — a different kernel subsystem, or a different set of userspace components. That is a design choice, not a conformance failure.

Note

The consequence of the second and third properties together is that conformance is checked against the protocol, never against Mainline. A system that speaks a standard correctly conforms to it, whatever it runs behind the channel and however it reaches its answers.

1.2 Conventions

Peios / Peios technical reference / PGSS / Introduction

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119. Text set off as a note is informative, not normative.

Everything else — roles, byte order, sizes, layout tables, notation, strings, timestamps, citation, and the external standards this anthology depends on — is defined in the Conventions book and is not restated here. PGSS departs from none of it.

Where a chapter needs a convention of its own, that chapter states it.

2.1 Scope and Roles

Peios / Peios technical reference / PGSS / Logon

This chapter specifies PGSS Logon: the protocol by which a caller obtains a KACS token and a logon session from an authentication authority, and the protocol by which any program on the system resolves an identity it already holds into a name, a SID, or the attributes a POSIX program expects of one.

Two roles participate.

The authority is the process that listens on the logon socket. It decides whether a logon succeeds, mints the resulting token, creates the logon session, and answers identity lookups. There MUST be at most one authority on a running system.

The client connects and speaks on a principal's behalf. It proposes what kind of logon it wants, renders prompts and returns answers without interpreting them, and receives the token and installs it itself. A client is not trusted: everything it sends is a claim (§2.4).

The principal is the identity a logon is for — the person or service being authenticated. The principal is not a party to the conversation.

Both roles are publicly implementable. A third party MAY ship a different authority, a different logon originator, or both, and interoperate with the other half unchanged.

This chapter covers:

  • the two channels an authority offers, and the access control governing each (§2.5, §2.14)
  • message framing, header layout, and the rules under which the format may be extended (§2.6)
  • the messages of the logon conversation, their fields, and their encodings (§2.7 to §2.10)
  • the shape of a conversation: who speaks, in what order, and how it terminates (§2.3)
  • the transfer of the minted token to the caller, and the session-profile values a logon originator needs in order to start a session (§2.9)
  • credential handling obligations binding on both roles (§2.11, §2.12)
  • what an authority MUST establish for itself rather than believe from a message (§2.4)
  • resolving a principal to a name, a SID, a POSIX identifier, or the attributes a POSIX program expects of one (§2.13 to §2.18)
  • how a bare name is resolved when more than one source could answer it (§2.15)
  • how a service manager obtains a token for a service, where there is no credential to exchange (§2.19)
  • how a principal changes their own credential (§2.20)
  • the obligations binding on each role (§2.21)
  • how a caller asks the authority to end a logon session — its own, or, where policy permits, somebody else's (§2.22)
  • how a principal adds a credential of their own, such as an SSH public key, or removes one (§2.23)

This chapter does not cover:

  • Tokens, SIDs, privileges, integrity levels, logon sessions, logon types, and access checks — described in the Peios Kernel TRM. SIDs and security descriptors are specified in PCDS.
  • How an authority decides whether a credential is valid, where it keeps identity, which principals exist, or what they are called. Those belong to the authority's own design. Mainline's authority federates them over PSI, the Principal Source Interface, specified in PSPU §2.
  • Credential enrolment — giving a principal a credential it does not have — and account administration, including setting a credential on another principal's behalf. Neither is specified.
  • Service startup and ordering, described in the peinit TRM.

The distinction in the second point is the load-bearing one. PGSS Logon specifies how a caller asks and how an authority answers. It says nothing about how the authority reaches its answer, because that is exactly what different systems will do differently, and constraining it would make the standard a description of one implementation rather than a contract between two.

Note

Mainline's authority is authd, which federates authentication to separate principal-source processes over PSI (PSPU §2). That architecture is one way to satisfy this chapter. An authority that consults a single built-in database, or a remote directory, or a hardware token reader, conforms equally well provided it speaks this protocol correctly.

2.1.1 What the authority does not do #

An authority MUST NOT be a process factory. It does not fork the principal's shell, does not learn about controlling terminals, environments, or session leadership, and does not decide what the caller does with the token it receives.

The caller installs the token and proceeds. This keeps the most privileged process on the system out of the business of launching arbitrary programs, and it means a client can obtain a token for a purpose the authority need never have anticipated.

The authority still starts nothing when it is asked to end a logon session (§2.22). It MAY then end the processes running in that session, and only those: ending what exists launches nothing, and the prohibition is on launching.

Note

This is why AccessGranted carries a descriptor and a session identifier and nothing else. There is no field describing what should happen next, because nothing about what happens next is the authority's concern.

2.1.2 Authentication and derivation #

Two acts, deliberately separated.

Authentication establishes that the principal is who they claim. Its output is an identity and nothing more.

Derivation constructs the token: which SIDs it carries, which privileges, at what integrity level, with what projected identifiers. Its inputs are the authenticated identity and local policy.

The separation matters because the second is where a machine's own rules apply. An identity established elsewhere — by a directory, by a remote authority — does not carry entitlements onto this machine with it. What that identity means here is decided here, every time, by the authority applying local policy.

An authority MUST perform derivation itself. It MUST NOT accept a token, a privilege set, or an integrity level supplied by any other party, whatever its trust level.

2.1.3 The token this chapter does not describe #

This chapter governs the conversation and the delivery of its result. It does not govern the contents of the token that results. What SIDs, privileges, and integrity level a principal receives is derivation, and derivation is the authority's judgement applied to local policy.

A caller that receives a token from this protocol has been told who it may act as. It has not been told, and MUST NOT infer, anything further about how that conclusion was reached.

2.2 Terminology

Peios / Peios technical reference / PGSS / Logon

Terms defined in the Peios Kernel TRM (token, logon session, privilege, integrity level, logon type) and in PCDS (SID, security descriptor, DACL) are used here with the same meaning and are not redefined.

Authority. The process that listens on the logon socket, decides whether a logon succeeds, mints the resulting token, creates the logon session, and answers identity lookups. There is at most one authority on a running system.

Client. A process that connects to the authority. Also called the originator when the emphasis is on whose identity the authority verifies. There are four client roles — originating a logon, changing the caller's own credential, looking an identity up, and ending a logon session — and they are independent (§2.21).

Principal. The identity a logon is for — the person or service being authenticated. The principal is not a party to the conversation; the client speaks on their behalf.

Conversation. One connection's exchange, from the client's opening message — a logon's, a credential change's (§2.20), an enrolment's (§2.23), or a request to end a logon session (§2.22) — to a terminal message from the authority. One connection carries exactly one conversation.

Round. One CredentialRequest from the authority and the CredentialResponse that answers it. A conversation MAY take several rounds.

Terminal message. AccessGranted or AccessDenied for a logon; CredentialChanged or AccessDenied for a credential change (§2.20) or an enrolment (§2.23); SessionEnded, SessionEndAllowed or AccessDenied for a request to end a logon session or to ask whether one may (§2.22). Exactly one is sent, and nothing follows it.

Credential material. Any byte sequence a principal supplies as proof of identity — a password, a one-time code, a response from a token. Distinguished throughout from a verifier, which is what an authority stores and which MUST NOT be usable as credential material.

Prompt. A request from the authority for one piece of credential material, carrying enough description for a client to render it without understanding what it is for.

Derivation. The authority's construction of a token's contents — its SIDs, privileges and integrity level — from the authenticated identity and local policy. Distinct from authentication, which establishes only that the principal is who they claim.

Attestation. A trusted process's assertion that a logon should happen, standing in for a credential where none exists and none should. Distinct from authentication, which establishes that a principal is who they claim by something only they could present; an attestation establishes only that the party asking is one the authority permits to ask (§2.19).

Source. A party an authority consults in order to answer a question about identity. Whether an authority has sources at all, and what they are, is its own design; the term appears here only where the protocol's behaviour depends on there being more than one possible answerer (§2.15, §2.18).

2.2.1 A note on "logon type" #

LogonType (§2.7) describes the kind of session being established — interactive, network, batch, service. It is a property of the situation, not of the principal, and the same principal may hold several sessions of different types at once. Its values and meanings are defined by KACS and described in the Peios Kernel TRM; this chapter carries it but does not define it. The values are listed for reference in §2.B.

2.3 The Conversation

Peios / Peios technical reference / PGSS / Logon

A logon is a conversation, not a call. The client opens it, the authority asks for whatever the principal's policy requires, and the authority ends it.

client                                   authority
  |                                          |
  |------------- LogonStart ---------------->|
  |                                          |
  |<--------- CredentialRequest -------------|   round 1
  |---------- CredentialResponse ----------->|
  |                                          |
  |<--------- CredentialRequest -------------|   round 2 (if required)
  |---------- CredentialResponse ----------->|
  |                                          |
  |<-- AccessGranted (+ token fd) -----------|   terminal
  |         or AccessDenied                  |

2.3.1 Why a conversation #

The value of this shape is that the client stays generic. It does not know what a password is. It renders the prompts it is given, collects the answers, and returns them.

That is what makes multi-factor authentication, password-expiry-forces-change, smartcards, or a policy that asks for a second factor only from an unfamiliar host, changes to authorities rather than to every client on the system. A protocol that named its credential kinds would have to be revised, and every client rebuilt, for each one.

Most logons are one round: the authority asks for everything the principal's policy requires, in one array, and decides. The conversational shape exists for what one round cannot express — choosing between authentication paths, or a shared account where one credential unlocks a requirement for another.

2.3.2 Sequence rules #

A conversation MUST proceed as follows.

  1. The client sends exactly one opening message: LogonStart, CredentialChangeStart to change its own credential (§2.20), CredentialEnrollStart to add or remove one of its own (§2.23), or SessionEnd or SessionEndQuery about a logon session (§2.22). It MUST be the first message. An authority MUST reject any conversation that opens with something else.
  2. The authority sends zero or more CredentialRequest messages. Each MUST be answered by exactly one CredentialResponse before the authority sends anything further.
  3. The authority sends exactly one terminal message: AccessGranted or AccessDenied for a logon, CredentialChanged or AccessDenied for a credential change or an enrolment, SessionEnded or AccessDenied for a SessionEnd, and SessionEndAllowed or AccessDenied for a SessionEndQuery.
  4. Both parties close the connection.

SessionEnd and SessionEndQuery exchange no credentials: the authority sends no CredentialRequest in reply to either, and the conversation is the opening message and its terminal.

The authority MAY send a terminal message at any point after LogonStart, including before any CredentialRequest. Zero rounds is a conforming conversation: an authority that can decide from LogonStart alone — a pre-authenticated caller, or a refusal on logon type — is not required to ask for anything.

A client MUST NOT send a CredentialResponse that was not solicited by a CredentialRequest. An authority MUST reject one that was not.

2.3.3 Bounding the conversation #

An authority MUST bound the number of rounds it will conduct and MUST bound the time it will wait for a CredentialResponse. Neither bound is fixed by this chapter, since both are policy. An authority that exhausts either MUST terminate with AccessDenied carrying ConversationLimit rather than closing silently, so that the client can distinguish a policy limit from a crash.

2.3.4 Termination #

Exactly one terminal message is sent. After it, the authority MUST NOT send anything further on that connection, and MUST close it.

A connection that closes without a terminal message is an abnormal termination. A client MUST treat it as a failed logon and MUST NOT retry automatically, since the reason is unknown and may be a policy refusal the authority could not express.

2.4 What an Authority Must Not Trust

Peios / Peios technical reference / PGSS / Logon

Nothing a client sends is trusted. This section states the rule once, because every field in §2.7 is subject to it.

2.4.1 Peer identity #

An authority MUST establish the connected peer's identity from the connected socket, by reading the peer's token from the kernel. It MUST NOT take the peer's identity from any message body, because there is no field in which a client could put it that the client could not also lie in.

An authority MUST NOT use SO_PEERCRED for this purpose. It answers a similar-looking question and is the wrong answer: it returns the projected UID, which cannot distinguish an authenticated principal from an unauthenticated process running under the same projection, and carries none of the token's SIDs, groups, integrity level or privileges.

Note

On Peios many principals project to the same identifier, so a projected UID frequently identifies nobody in particular. Two different principals can share one. The peer token is the only thing that answers "who is this?" exactly.

2.4.2 Logon type #

LogonStart.logon_type is a proposal, not an instruction.

Only the caller knows whether an inbound connection is an interactive shell or a batch command, so the caller has to be the one to say. But an authority MUST constrain the proposal against what that verified peer is permitted to request. Otherwise anything that can reach the socket can mint itself an interactive session, and the logon type — which access control decisions depend on — becomes a value chosen by the least trusted party in the exchange.

2.4.3 Identifier #

The identifier names the principal a logon is for. It is a claim about who is being authenticated, and it is what the subsequent credential exchange exists to test. An authority MUST NOT treat an identifier as established until authentication has succeeded.

2.4.4 Everything else #

tty and remote_host are unverified context. An authority MAY record them, MAY use them in policy, and MUST NOT treat them as established facts. A client that lies about its remote host is not prevented from doing so by this protocol.

2.4.5 Access to the socket #

Access to the logon socket MUST be controlled by a security descriptor.

It MUST NOT be controlled by process integrity level. PIP can gate a socket, but using it here would require every future caller — a graphical greeter, a web console, a remote access daemon — to be signed at high trust merely to collect a password. Collecting a credential is not a privileged act; deciding whether it is correct is, and that decision happens on the other side of the socket.

The descriptor decides who may reach the socket, not what a peer may do once there. Where it admits principals so that they can change their own credential (§2.20) or enrol one (§2.23), it admits peers that may not originate logons, and an authority MUST decide from each peer's identity whether it may originate one.

Rate limiting is defence in depth. It is not the access control, and an authority MUST NOT rely on it as such.

2.5 The Logon Channel

Peios / Peios technical reference / PGSS / Logon

2.5.1 Socket #

An authority MUST listen on a SOCK_STREAM Unix domain socket at:

/run/logon.sock

The path is normative. It is the standard's path, not an implementation's, and a client MUST NOT require configuration to find it.

2.5.2 Access control #

The socket MUST carry a security descriptor granting connect access to the principals permitted to originate logons, to the principals permitted to change their own credential (§2.20) or enrol one (§2.23), and to those permitted to end a logon session (§2.22). See §2.4 for why this, and not process integrity, is the control — and why reaching the socket is not, on its own, permission to originate a logon.

2.5.3 One conversation per connection #

A connection carries exactly one conversation. The connection is the conversation's identity.

There is therefore no correlation identifier in the header, and none is needed: a message belongs to the conversation it arrived on. This removes a class of error and attack — a forged or confused identifier cannot attach a credential response to somebody else's logon, because there is no identifier to forge.

It also bounds the credential's lifetime by the connection's, which makes that the kernel's job to enforce rather than the authority's to remember.

An authority MUST close the connection after sending its terminal message. A client MUST close after receiving one.

A ServiceAttest (§2.19) is not a conversation and exchanges no credential, but it is bound by the same rule: one per connection, answered with one terminal message, and the connection closed. It is carried here rather than on its own socket because it is a logon in every respect except how the authority was satisfied — it creates a session and mints a token — and because the volume is a handful of requests at start-up rather than the sustained load §2.14 exists to separate.

SessionEnd and SessionEndQuery (§2.22) are bound by the same rule: one request per connection, one terminal message, and the connection closed. They are here because ending a session is a decision about logons, made by the party that created the session.

Note

A protocol whose connections are long-lived and shared reaches the opposite conclusion, and multiplexes. PSI does (PSPU §2.7), and so does this chapter's own identity channel (§2.14). The difference is not inconsistency: a logon client's connection exists for one logon and is discarded, so paying for multiplexing here would buy nothing and a correlation identifier would only add something to forge.

2.5.4 Concurrency #

An authority MUST serve conversations concurrently. A logon that stalls — a principal who walks away mid-prompt — MUST NOT prevent other logons from proceeding.

An authority MUST bound the number of conversations it will serve at once, and MUST bound the time a conversation may remain open. Both are policy; neither is fixed here.

2.5.5 Descriptor passing #

The channel MUST support ancillary data (SCM_RIGHTS). The token is transferred as a file descriptor alongside AccessGranted — see §2.9.

2.6 Message Framing

Peios / Peios technical reference / PGSS / Logon

Both of an authority's channels use the framing specified here without modification. The identity socket's departures are stated in §2.14; they concern which message types are served where, not the encoding.

2.6.1 Header #

Every message begins with a 12-byte header:

OffsetSizeFieldValue
04magicPGSL (50 47 53 4c)
42version1
62msg_typeSee §2.A
84total_lenHeader plus body, in bytes

total_len counts the header. A message is therefore self-delimiting from its first 12 bytes, and a reader can size its buffer before reading the body.

2.6.2 Magic #

The magic MUST be checked on every message and MUST cause an immediate, whole-connection failure when wrong.

This is not decoration. Peios protocols share this codec and this header layout, and some of them share message bodies. A socket plugged into the wrong daemon would otherwise partially work, which is far worse than failing outright — a subtle misbehaviour several fields in, rather than a hard error on byte zero. A protocol reusing this codec MUST take a distinct magic.

2.6.3 Version #

A peer receiving a version it does not implement MUST refuse the message. An authority MUST refuse with AccessDenied carrying UnsupportedVersion where it can still encode one.

2.6.4 Size limit #

A message MUST NOT exceed 65536 bytes in total. A decoder MUST reject a header declaring more, without reading the body, and MUST reject one declaring fewer than the header's own length.

2.6.5 Message type #

The high bit of msg_type marks a message sent by the authority. Client messages have it clear. This is a readability property rather than a security one — a peer MUST validate the message type it received against what it expected, not merely against the direction bit.

2.6.6 Encoding #

All multi-byte integers are little-endian.

Strings are UTF-8 and are not NUL-terminated. A string is encoded as a u32 byte count followed by exactly that many bytes. A decoder MUST reject a string whose bytes are not valid UTF-8.

An empty string and an absent optional string are encoded identically, as a zero byte count. A field documented as optional is therefore absent when empty, and an implementation MUST NOT distinguish the two.

Byte strings are encoded the same way and carry no encoding requirement.

Arrays are encoded as a u32 element count followed by that many length-framed structures. Every array in this chapter has a stated maximum; an encoder MUST refuse to produce a longer one and a decoder MUST reject one it receives. The same holds for every stated byte limit.

Two distinct length mechanisms therefore appear, and confusing them is the most likely implementation error:

  • Byte strings and strings are prefixed with a u32 byte count, and the bytes follow immediately.
  • Structures and array elements are prefixed with a u32 byte count of the whole structure, and a decoder MUST skip to the structure's declared end after reading the fields it knows.

The second is what makes the format extensible.

2.6.7 Body extensibility #

The body is a single length-framed structure, and every structure and every array element within it is length-framed too.

A decoder reads the fields it knows and then skips to the structure's declared end. A field appended by a newer peer is therefore stepped over rather than mistaken for the next field — which is what makes the format extensible inside arrays, where ignoring trailing bytes at the message level would not help.

A structure that ends before a field an older encoder never wrote is not an error. A decoder that reaches the end of a structure's body where an optional trailing field would have been MUST substitute that field's documented default (§2.7, §2.9) rather than failing.

The rules this keeps working under are binding on anyone revising this chapter:

  • Fields are appended only. Never reordered, never removed, never changed in meaning.
  • A new field MUST be optional with a safe default, because older peers will not send it and will not read it.
  • Adding a value to an enumeration is a breaking change and requires a version bump, because every peer is required to understand every value it is sent.

The last rule is the one that surprises people. An unknown enumeration value cannot be safely ignored: a client that skipped an unrecognised credential type would silently fail to collect something the authority required, and an authority that skipped an unrecognised logon type would grant the wrong kind of session.

There are exactly four exceptions, and each is stated where it applies: LogonStart.supported_credential_types, which is a statement of capability rather than an instruction (§2.7); LogonStart.logon_type, which only the authority ever receives (§2.7); the field mask of Lookup, whose reply says which bits it answered (§2.16); and a denial code that only ever answers a message newer than every peer that does not know it, so that no such peer is ever sent it (§2.B, §2.22, §2.23).

2.6.8 Initial unpublished layout replacement #

The coordinated SSH credential extension replaced the unpublished version-1 layout without retaining older codecs. Current LogonStart capability, required-method and binding fields, Prompt parameters, and all current Assertion fields are mandatory, including explicit empty/zero values. Omitted mandatory fields fail decoding. The compatibility rules above apply to future extensions of this replacement contract; they do not require accepting the superseded development layout. See PGSS Logon §2.D.

2.7 LogonStart

Peios / Peios technical reference / PGSS / Logon

msg_type = 0x0001. Client to authority. Opens the conversation; MUST be the first message.

2.7.1 Layout #

FieldEncodingLimit
logon_typeu8§2.B
identifier_typeu8§2.B
identifierlength-framed bytes1024
ttystring128
remote_hoststring256
supported_credential_typeslength-framed bytes, one u8 per type32
required_credential_typeu8; 0 none, 1 Password, 2 SshPublicKey§2.D
has_ssh_bindingu8, exactly 0 or 1
ssh_bindingstructure, present when the flag is 1§2.D

Everything here is asserted by the client, and §2.4 governs all of it.

2.7.2 logon_type #

The kind of session the client is asking for. A proposal, which the authority MUST constrain against the verified peer — see §2.4.

Values are defined by KACS and listed for reference in §2.B.

2.7.2.1 The exception this field carries #

A value MAY be added to this enumeration without a version bump. This is the third departure from §2.6, and it turns on the same question that rule turns on: which party has to understand the value. The rule binds because every peer must understand every value it is sent, and logon_type only ever travels client to authority. The authority is therefore the only party that must understand a new one.

An authority MUST refuse a logon_type it does not recognise, with MalformedRequest (§2.10). A message carrying a value the authority cannot name is one it could not understand, and it is refused before any question of what the peer is permitted arises.

Neither direction of mismatch is left to chance by that. An older client never proposes a value it has not heard of, and a newer client proposing one to an older authority is refused — so the failure is no session rather than a session of the wrong kind, which is the outcome §2.6 exists to prevent.

2.7.3 identifier_type and identifier #

Together these name the principal. identifier_type says how to read identifier; identifier is opaque bytes, not a string.

Bytes rather than a string because an identifier is not always text. A certificate thumbprint, a smartcard serial, or a binary principal name are all reasonable identifiers, and a protocol that insisted on UTF-8 would exclude them. An authority that expects text MUST validate the encoding itself.

An identifier MAY be empty. An empty identifier means the principal is not named here — the authority is expected to determine it from the credential, as with a smartcard that carries its own identity.

identifier_type is distinct from credential_type (§2.8): this is the claim of identity, that is the proof. A passkey names a principal and proves them in one artefact; a username names them and proves nothing.

2.7.4 tty and remote_host #

Unverified context, both optional, both empty when absent.

tty names the terminal the logon is happening on, where there is one. remote_host names where a network logon came from.

An authority MAY use either in policy and MAY record either in its audit trail. It MUST NOT treat either as established. A client that lies about them is not prevented from doing so.

2.7.5 supported_credential_types #

Every credential type this client can render, one byte each.

This is the client declaring its capabilities, and it is binding on the authority: an authority MUST NOT send a prompt for a credential type absent from this list (§2.8).

A client that supports nothing sends an empty list. That is meaningful rather than degenerate — it says "I can complete a logon that requires no interaction, and nothing else" — and an authority MUST either complete the logon without prompting or deny it.

2.7.5.1 Mandatory capability and method fields #

The capability list and following method/binding fields are mandatory in the current unpublished version-1 layout. Missing tails are malformed; there is no absent-field Password default. An explicit empty list means that the client cannot collect a credential.

A nonzero required method must appear in the capability list. The authority rejects any source assertion that did not authenticate with that method. The SSH binding is present exactly when SshPublicKey is advertised. It is an enclosing length-framed structure containing 16 raw connection-ID bytes, a session-identifier byte string (16–64 bytes), and a UTF-8 username (1–256 bytes, no controls), equal to the Username identifier byte for byte. Only the authorized SSH monitor may supply it; see §2.D.

An unrecognised value is dropped, not refused. A decoder MUST discard a credential type it does not recognise and proceed with the rest, leaving the intersection of what the client claims and what the decoder understands. This is safe here and nowhere else, because the field is a statement of capability rather than an instruction: an authority that ignores a type it has never heard of merely declines to use it, which is the correct outcome.

Without this, a client that learned a new credential type could not speak to an older authority at all — the authority would be obliged by §2.6 to refuse the whole message. The capability list exists precisely so that an authority using a new type cannot reach an older client; it would be self-defeating if a client using a new type could not reach an older authority either.

2.8 Credential Exchange

Peios / Peios technical reference / PGSS / Logon

2.8.1 CredentialRequest #

msg_type = 0x8001. Authority to client.

FieldEncodingLimit
messagesarray of Message8
promptsarray of Prompt16

Message:

FieldEncodingLimit
severityu8§2.B
textstring512

Prompt:

FieldEncodingLimit
credential_refu32—
credential_typeu8§2.B
credential_namestring128
parameterslength-framed bytes4096

Both arrays MAY be empty.

2.8.1.1 Messages #

Text for the principal to read, before any prompt is presented. "Your password expires in three days"; "Authenticating against the local source".

A client MUST display messages it receives, in order, before the prompts in the same request. It MUST NOT interpret them, and MUST NOT vary its behaviour on their content.

2.8.1.2 Prompts #

Each prompt asks for one piece of credential material.

credential_ref identifies the prompt within the conversation. The client echoes it back unchanged in its answer. It MUST be unique among the prompts of a single request. An authority MAY reuse a value in a later round.

credential_name is what to show the principal — "Password", "Verification code". It is a display string. A client MUST NOT branch on it; credential_type is what says how to collect the answer.

2.8.1.3 The client capability rule #

An authority MUST NOT send a prompt whose credential_type is absent from the client's supported_credential_types (§2.7).

This is a hard requirement rather than a courtesy. A client that receives a prompt it cannot render has no good option: failing the logon punishes the principal for a mismatch neither party chose, and guessing — collecting a line of text for a credential type that is not a password — may echo a secret to the screen. An authority that cannot proceed within the client's declared capabilities MUST deny the logon instead.

A client that nevertheless receives an unrenderable prompt MUST fail the conversation rather than guess.

2.8.1.4 Empty requests #

A CredentialRequest with no prompts is valid. It carries messages only, and the client MUST answer it with a CredentialResponse carrying no answers. This is how an authority conveys information mid-conversation without asking for anything.

2.8.2 CredentialResponse #

msg_type = 0x0002. Client to authority.

FieldEncodingLimit
answersarray of Answer16

Answer:

FieldEncodingLimit
credential_refu32—
datalength-framed bytes32768

credential_ref MUST match a prompt from the request being answered. data is the material the principal supplied — opaque bytes, no encoding implied.

A client SHOULD answer every prompt it was given. An authority MUST NOT assume it has: a missing answer is a failed logon, not a protocol violation, and MUST be treated as such rather than as grounds to tear down the connection.

An authority MUST match answers to prompts by credential_ref and MUST NOT rely on ordering.

Both parties encode and decode this message under the obligations of §2.12. The encoded message holds the credential just as much as the data field inside it does.

Note

A client that cannot collect an answer — the principal pressed escape — MAY send a response omitting it rather than closing the connection. That gives the authority the opportunity to deny cleanly with a reason the principal can read.

2.8.3 Typed parameters #

Prompt parameters are mandatory, empty for Password, and carry the bounded SshPublicKey disposition structure described in §2.D. They are protocol input to the matching collector, never instructions to render or execute. An ordinary password client must refuse an unsupported SSH prompt.

2.9 AccessGranted

Peios / Peios technical reference / PGSS / Logon

msg_type = 0x8002. Authority to client. One of the two terminal messages; nothing follows it.

FieldEncoding
session_idu64
profilelength-framed structure, optional

Ancillary data: the token, as one file descriptor via SCM_RIGHTS.

2.9.1 The descriptor #

The token MUST be transferred as a file descriptor attached to this message. It MUST NOT be named, and there MUST be no path, handle, or identifier by which another process could reach it.

Possession of the conversation is what confers the token. There is no window in which a minted token exists under a name something else could open, and no lookup that could be raced or guessed.

A client MUST read the descriptor from the ancillary data of this message. An AccessGranted arriving without one is a protocol violation and MUST be treated as a failed logon — a client MUST NOT proceed as though a logon succeeded when it holds no token.

2.9.2 session_id #

The logon session the token belongs to. The client MAY record it, MAY report it, and needs it to relate this logon to the kernel's session records.

It is informational to the protocol. The token is the thing that confers authority; the session identifier merely names the session the token already belongs to.

2.9.3 profile #

Where the session starts, and what to call the principal. A length-framed structure, so it can grow without displacing anything appended to AccessGranted after it.

FieldEncodingLimit
homestring4096
shellstring4096
display_namestring256

profile is an optional trailing field. Every field may be empty, and an empty field means the authority did not say. An authority that knows nothing about home directories is conforming, and so is one that omits the whole structure — a client MUST treat an absent profile exactly as it treats one whose fields are all empty.

A client MUST have a fallback for each field and MUST NOT treat an empty value as an error.

home and shell, when non-empty, MUST be absolute paths. A client MUST NOT execute a relative shell or resolve a relative home against its own working directory. An authority with no value for a field MUST leave it empty rather than invent one.

2.9.3.1 Why this is on the terminal message #

Nothing here is an access-control input. No ACL names a home directory, and the token carries none of these fields — so this is not identity, and an authority that got it wrong would produce an inconvenient session rather than an unsafe one.

It is here because the authority has just read these values in order to decide the logon, and a caller that needs them in order to start a session would otherwise have to ask a second time. A logon originator cannot chdir or exec without them.

2.9.3.2 What this is not #

It is not a directory lookup, and it MUST NOT be treated as one. It answers for exactly one principal — the one who just authenticated — at exactly one moment. It cannot answer for anybody else, it cannot answer for a principal who never logged on, and nothing in this protocol says what it means an instant later.

Resolving arbitrary principals to home directories or shells is §2.16's, and a client that caches these values as though they had come from there has misread them.

2.9.3.3 display_name #

A human's name for a human to read.

It is deliberately not a GECOS field. GECOS is a comma-separated /etc/passwd artefact carrying a name, an office, and two telephone numbers in one string; a protocol that carried one would oblige every client to parse it apart, and would tie this chapter to a file format it has nothing to do with.

An implementation that must produce a GECOS field renders it from this, not the other way round.

Note

A client is not obliged to use any of this. Falling back to a fixed shell, or ignoring display_name entirely, is conforming. The fields exist so that a client which wants them does not need a second protocol.

2.9.4 What the client does next #

Installs the token, if that is what it wanted. The authority is not involved and MUST NOT be told (§2.1).

A client that decides not to install the token MUST close the descriptor. A logon session whose token is never installed still exists as far as the kernel is concerned.

A client SHOULD NOT fail a logon because a home it was given does not exist. The principal has authenticated and holds a token; refusing to start their session over a missing directory turns a cosmetic problem into being locked out. Starting elsewhere and saying so is the better failure.

2.10 AccessDenied

Peios / Peios technical reference / PGSS / Logon

msg_type = 0x8003. Authority to client. The terminal failure message of a logon, of a credential change (§2.20), of an enrolment (§2.23), and of a request to end a logon session or to ask whether one may (§2.22) alike; nothing follows it.

FieldEncodingLimit
denialu32§2.B
reasonstring512

2.10.1 denial #

A code from a deliberately small vocabulary (§2.B), for a client that needs to act differently — retry, offer a different account, report a system fault.

PermissionDenied and LogonTypeNotPermitted are distinct on purpose: the first says the peer may not use this socket for this at all, the second that it may originate logons but not of this kind. A client can act differently on each.

NoSuchSession answers only SessionEnd and SessionEndQuery, and only a caller who would otherwise have been permitted (§2.22). Anybody else is told PermissionDenied, so the code cannot be used to learn which sessions exist.

CredentialRejected answers only CredentialEnrollStart, and says the credential offered or named was refused, not the principal's proof of who they are (§2.23).

2.10.2 reason #

Text for the principal to read. A client SHOULD display it and MUST NOT interpret it.

The division is the same one CredentialRequest makes between credential_type and credential_name: the machine-readable field is small and stable, the human-readable one is free.

reason MUST NOT narrow an AuthenticationFailed into a specific cause.

2.10.3 What a denial must not reveal #

The denial vocabulary deliberately does not distinguish "no such principal" from "wrong credential". Both are AuthenticationFailed.

A protocol that distinguished them would make account enumeration a supported feature: anyone able to reach the socket could test names and learn which exist. An authority MUST NOT provide that distinction through the denial code, through reason, or through observable timing.

The timing obligation is the one most often missed, and it binds even though this chapter cannot check it. An authority whose unknown-principal path returns faster than its wrong-credential path has published the distinction it just declined to state — see §2.12.

Note

An authority MAY record the distinction in an audit trail an administrator can read. What it must not do is return it to the caller.

2.11 Credential Types

Peios / Peios technical reference / PGSS / Logon

A credential type tells a client how to collect an answer. It does not tell it what the answer means.

ValueNameCollection
1PasswordA line of text, not echoed
2SshPublicKeyBound SSH candidate or signature proof; §2.D

The registry is deliberately short. A type is added when a client would need to collect something differently, not when an authority acquires a new way of checking something.

Note

A one-time code is collected exactly as a password is — a line of text, not echoed — so it needs no new type. An authority asks for it with credential_type = Password and a credential_name of "Verification code". A hardware token that requires a challenge to be relayed to a reader would need a new type, because no existing collection method produces the answer.

This is the test to apply: does an existing client, understanding only the types it already has, collect the right thing? If yes, no new type is needed.

2.11.1 Adding a type #

The initial unpublished type-2 extension replaced the version-1 layout and all consumers together (§2.D). Future incompatible changes to a published contract require the version discipline in §2.6.

An authority MUST NOT send a prompt for a type absent from the client's supported_credential_types, so an authority using a new type simply cannot reach an older client — which is the correct outcome, and is why the capability list exists. The converse — a newer client reaching an older authority — is what the dropping rule in §2.7 exists for.

2.11.2 Why passwords cross the local socket in the clear #

Passwords travel this local socket as plaintext. SSH public-key authentication instead carries public keys and signatures; private keys never cross it. Remote SSH transport encrypts both authentication methods. The local password choice allows the source to retain a memory-hard password verifier without storing a password-equivalent secret.

Challenge-response requires the verifier to store something it can recompute the response from: either the plaintext, or a value that is password-equivalent. NTLM works exactly this way — the stored hash is as good as the password, forever, which is why pass-the-hash has been the most valuable credential on a Windows network for twenty years.

A modern verifier — argon2id and its relatives — is deliberately not password-equivalent and cannot answer a challenge. That is the property that makes stealing the store meaningfully weaker than knowing the passwords.

So challenge-response would trade a permanent weakness at rest for protection of a channel that is not the weak point. Anyone able to read this socket can already ptrace the process holding the password.

2.11.2.1 What would change the answer #

A PAKE — OPAQUE, SRP — gives both properties at once: nothing password-equivalent at rest, and no plaintext on the wire. It is worth revisiting if an authority ever authenticates across a network without TLS.

It is not worth its complexity for a local, kernel-mediated socket, where the threat it defends against is already able to do worse.

2.11.2.2 What follows #

Because plaintext crosses the wire, bounding how long it survives becomes an obligation of both roles rather than a nicety. See §2.12.

2.12 Credential Handling

Peios / Peios technical reference / PGSS / Logon

These bind both roles. They are stated normatively because plaintext on the wire (§2.11) is only defensible if its lifetime is short.

2.12.1 Bounding lifetime #

An implementation MUST hold credential material in memory that is erased before it is released, and the erasure MUST NOT be removable by an optimising compiler.

The obligation extends to the encoded message, not only to the credential field. A CredentialResponse holds the password just as much as the answer inside it does, and a buffer wiped at one level while another copy is dropped unerased achieves nothing.

An implementation MUST erase:

  • the buffer credential material was read into;
  • any buffer it was copied into during encoding or decoding, including an intermediate allocation abandoned by a buffer that grew;
  • any structure holding it once the conversation reaches a terminal state.

The middle clause is the one an implementation is most likely to miss. A growable buffer that reallocates while a message is being encoded leaves a complete copy of everything written so far in the abandoned allocation, and erasing the buffer that survives does not touch it. Reserving the encoded size before writing avoids the problem entirely.

2.12.2 What this does not promise #

It guarantees that these buffers are erased before their memory is reused. It cannot guarantee anything about copies made elsewhere — a string the caller parsed a credential out of, a register or stack slot the optimiser chose, a terminal's own input buffer. Those belong to whoever made them.

Nor does it defend against a hostile kernel, a core dump, or swap. Those are addressed elsewhere: process integrity protection, and disabling core dumps for the authority.

2.12.3 Logging and diagnostics #

An implementation MUST NOT write credential material to a log, an audit record, an error message, or a debugging aid.

A type carrying credential material SHOULD render, under whatever debug formatting its language provides, as a redaction rather than as its contents. A stray diagnostic print is the most common way material escapes a process that was otherwise careful, and the defence is to make the careless thing produce nothing useful.

2.12.4 Collection #

A client MUST collect a Password without echoing it. Where it cannot establish that echo is suppressed, it MUST NOT collect the credential anyway.

A client MUST NOT silently truncate an answer. data admits 32768 bytes (§2.8); a client whose collection method admits fewer MUST fail the conversation rather than send a prefix of what the principal supplied, which would present as a wrong credential and leave the remainder in whatever buffer it was read from.

2.12.5 Timing #

An authority MUST NOT allow the time it takes to reach a denial to distinguish an unknown principal from a bad credential (§2.10).

This is more demanding than it looks with a memory-hard verifier, because the natural implementation returns immediately when a principal does not exist and spends tens of milliseconds when one does. An authority MUST perform equivalent work in both cases — verifying against a decoy verifier that no credential matches is the usual construction.

Note

A decoy derived from randomness at start-up, rather than from a constant, is not merely unguessable but not stable between boots either. An implementation built this way runs exactly one verification on both paths, including for a principal that requires no credential at all.

2.13 Identity Lookup

Peios / Peios technical reference / PGSS / Logon

Sections 2.3 to 2.12 specify how a caller obtains a token. The remainder of this chapter specifies how any program on the system turns an identity it already holds into something it can display or compare: a name into a SID, a SID into a name, a POSIX identifier into either.

It exists because a Linux program calls getpwuid and has never heard of a token. Without this surface, every principal an authority mints appears throughout the system as a bare number.

2.13.1 Why the authority answers this #

An authority may federate identity to separate sources, and a source counts POSIX identifiers relative to a range the authority assigns it. The authority adds the base. No message in either direction carries an absolute identifier to or from a source: a source asserts relative numbers, and is asked by relative number or by SID.

The arithmetic that makes a number absolute therefore exists in exactly one place, and only that place can invert it. No source can be asked "who is uid 1001000", because no source is ever asked an absolute number at all. The authority is not merely a convenient place to put this — it is the only party the protocol permits to answer.

The same holds for names. A bare name may exist in more than one source, and which one wins is a property of the system rather than of any source in it (§2.15).

Note

An authority that keeps all identity in one built-in database still satisfies this, and for it the rebasing question does not arise. The requirement is on the answer, not on how the answer is reached — as everywhere else in this chapter.

2.13.2 A second socket, not a second standard #

Identity lookup is served on /run/ident.sock (§2.14), separately from /run/logon.sock, by the same authority speaking the same framing (§2.6).

The separation is not for isolation. One authority answers both, so a defect or a hang in either reaches the other regardless; a second socket buys nothing there and this chapter does not pretend otherwise.

It is for admission. A listening socket has one accept queue. A single directory listing is thousands of lookups and a filesystem walk is millions, where logons are a handful per boot — so a shared socket would let an ordinary find fill the queue that an administrator needs in order to sign in and stop it. Two sockets means the two populations of caller cannot starve each other, whatever load either is under.

The second reason is access control. The set of programs that may originate a logon is small and enumerable; the set that may look up a name is every program on the system. Those want different security descriptors, and a descriptor is a property of a socket.

2.13.3 Not a conversation #

A logon is stateful: the connection is the conversation, and needs no correlation identifier (§2.5).

A lookup is not. Requests are independent, a connection carries as many as a client cares to send, and replies MAY return in an order other than the one requests arrived in. Each request therefore carries a tag that its reply echoes (§2.14).

Note

That is what allows an authority to satisfy several outstanding requests from one connection together — against a remote source, in a single query — without a delay window that would tax a caller waiting on a single answer. Nothing here requires it, and an authority that answers strictly in order, one request at a time, is fully conformant.

2.13.4 Not authentication #

No credential ever crosses this socket. There is no message with which a client could offer one and none with which an authority could ask.

Everything here returns the kind of data a POSIX system has historically kept in a world-readable file. An authority MAY nonetheless restrict individual fields (§2.16), and the request names the fields it wants precisely so that it can.

2.13.5 Not in this version #

Privileges, integrity levels, owner and default DACL are not returned by any message here. They are not identity: nothing stores them, and an authority computes them from local policy at the moment it derives a token (§2.1).

They are, however, deliberately reserved rather than excluded. An authority that cannot be asked what a policy would produce can only be verified by signing someone in and observing the result. A future revision is expected to add a distinct request of the shape:

Evaluate { key, logon_type } -> { privileges, integrity, owner, default_dacl }

— a token that is derived and then discarded. It is parameterised by logon type because the answer genuinely depends on it: an authority adds SIDs reflecting how a principal signed in (§2.16), so "what privileges does this principal have" has no single answer.

A revision MUST NOT instead add privileges or integrity as fields of Lookup (§2.16). A field of a record describes something a source holds; this does not.

2.14 The Identity Channel

Peios / Peios technical reference / PGSS / Logon

2.14.1 Socket #

An authority MUST listen on a SOCK_STREAM Unix domain socket at:

/run/ident.sock

The path is normative, for the same reason /run/logon.sock is: a name resolver linked into every process on the system cannot be asked to find it by configuration.

An authority MUST listen on both sockets. Offering one without the other is not conformance — a system whose principals cannot be named is as broken as one whose principals cannot sign in.

2.14.2 Access control #

The socket MUST carry a security descriptor granting connect access to every principal that runs ordinary programs.

This is the opposite posture to /run/logon.sock, and deliberately so. A descriptor that withheld connect access would not protect anything: it would make ls -l print numbers for the principals it excluded, while a principal who can connect learns the same names either way. Restriction, where an authority wants it, belongs on individual fields (§2.16) — not on reaching the socket at all.

2.14.3 Framing #

Messages use the header, magic, version, size limit and body extensibility rules of §2.6 without modification. The message types served here are disjoint from those of the logon socket (§2.A).

A message of one socket's range arriving on the other MUST be refused. An authority MUST NOT serve a Lookup received on /run/logon.sock, and MUST NOT serve a LogonStart received on /run/ident.sock.

Note

Sharing the magic is what makes that a clean refusal rather than a parse failure. A peer connected to the wrong socket gets "this message is not served here" from byte six, instead of discovering the mistake several fields into a structure it has misread.

2.14.4 Multiplexing #

A connection carries any number of requests. A client MAY have more than one outstanding at a time, and an authority MAY answer them in any order.

Every request carries a tag, a u32 chosen by the client, and its reply carries the same value. The tag is in the message body, not the header, so that the header of §2.6 is unchanged.

A client MUST NOT reuse a tag while a request bearing it is outstanding. An authority MUST echo the tag it received and MUST NOT interpret it otherwise.

A client MUST NOT assume replies arrive in request order. An authority that answers strictly in order is conformant; a client that depends on it is not.

Note

The contrast with §2.5 is deliberate rather than inconsistent. A logon connection exists for one logon and is discarded, so multiplexing would buy nothing and a correlation identifier would only add something to forge. A lookup connection is opened by a name resolver that may issue thousands of requests, and the identifier costs four bytes.

2.14.5 Concurrency and bounds #

An authority MUST serve connections concurrently, and MUST NOT let a slow answer on one connection delay answers on another.

An authority MUST bound the number of connections it will accept and the number of requests it will hold outstanding per connection, and SHOULD refuse further requests on a connection that exceeds the second rather than closing it.

An authority MUST NOT wait indefinitely for a source. A request that cannot be answered within the authority's own bound MUST be answered Unavailable (§2.18). The bound is on the request, not on each source consulted: an authority that walks several sources in turn MUST NOT let the total exceed what it would have allowed one.

2.14.6 No descriptor passing #

Nothing is transferred over this channel by descriptor. An authority MUST ignore ancillary data received here.

2.14.7 Peer identity #

An authority that restricts any field (§2.16) MUST establish peer identity from the connected socket's peer token, as §2.4 requires, and MUST NOT take it from a message body.

An authority that restricts no field MAY skip establishing peer identity, since it would make no decision with it.

2.15 Names and Resolution

Peios / Peios technical reference / PGSS / Logon

2.15.1 The name is opaque to the client #

A name is carried as a single string. A client MUST forward what it was given, unchanged, and MUST NOT parse, split, qualify, case-fold, or otherwise interpret it.

All interpretation is the authority's.

This is the load-bearing rule of the section. If a client resolved part of a name itself — chose which source to consult, or which of two candidates wins — then that policy would live in every client separately, and a native tool could resolve jack to a different principal than a POSIX name resolver on the same machine. Two components disagreeing about who a name refers to is not a display inconsistency; it is a program acting on one principal's behalf while checking another's access.

One authority means one answer, and it means a name resolves identically here and in a logon (§2.7), because the same resolution serves both.

2.15.2 Comparison #

Name comparison is ASCII case-insensitive.

This is normative rather than each source's own choice, because a system may have more than one source and they must not disagree about whether JACK is jack. An authority MUST establish the comparison for itself rather than delegate it to whatever a source happens to do.

2.15.3 Reserved characters #

A principal or group name MUST NOT contain any of:

CharacterReserved for
@Qualified names (below)
\Qualified names, in the form some callers will type
/Path separation — a name reaches a filesystem as a home directory
:Field separation in POSIX passwd and group records
,Member and subfield separation in POSIX group and GECOS records

A name MUST NOT contain a byte outside the printable ASCII range 0x20–0x7e, and MUST NOT begin or end with a space.

An authority MUST refuse to create a name violating these rules, MUST refuse one it is asked to resolve, and MUST refuse one asserted by a source. The third is the one that matters: a name reaching a caller from a source has crossed a boundary the other two never did, and it is the one that ends up in a passwd-format record, an audit line, or a log.

The excluded control characters are the record separators. A name carrying a newline could forge a whole line in a passwd-format file, an audit record, or a log — and the damage is done by the reader, so it cannot be prevented at the point the name is displayed.

The rules apply to every name an authority emits, including the names carried alongside SIDs in references (§2.16), not only to the name a request was keyed on.

Note

The ASCII restriction is not an assumption that names are English. It is a decision to defer confusable and normalisation handling rather than get them wrong: jack spelled with a Cyrillic а renders identically to jack and is a different principal, and the same name in NFC and NFD is two byte sequences that a comparison will call two people. Relaxing this later is backward compatible; tightening it later would mean renaming principals that already exist.

2.15.4 Qualified names #

A name MAY be qualified with a realm, written name@realm.

No realm syntax is defined in this version. An authority MUST refuse a name containing @, and the character is reserved so that a principal literally named jack@local cannot come into existence before the syntax does.

Nothing here changes when realms arrive: a qualified name is a different string in the same field, parsed by the same authority. That is why the field is one opaque string rather than a structured pair — a structure would have put the parsing in the client, which the first rule of this section forbids.

2.15.5 Search order #

A bare name is resolved by consulting sources in an order that is local configuration of the authority.

An authority MUST resolve in a configured order, MUST NOT derive that order from the order in which sources registered, and MUST stop at the first source that answers.

Note

Registration order is whatever the boot happened to do. Making it decide who jack is would mean a slow disk could change which principal a name refers to.

2.15.6 Answers an authority holds itself #

An authority MAY answer from its own knowledge, without consulting any source. Well-known principals and groups — those whose SIDs are fixed by the system rather than issued by anybody — are the ordinary case: their names, and, for those whose membership is a rule, the fact that nothing records it (§2.16), are properties of the system. Who is in a BUILTIN group is not: it is recorded by the sources, and asked of them (§2.16).

Such an answer is subject to every rule in this section. In particular an authority MUST apply the reserved-character and comparison rules to it, and MUST NOT return an answer for a name it would have refused.

An answer of this kind is not an outage and does not make a reply Unavailable, whatever the state of the configured sources. It is also not a source: an authority MUST NOT report it in the incomplete list of an enumeration (§2.17), and MUST NOT count it as a source having answered for the purpose of §2.18.

2.15.7 Bare in, qualified out #

Every successful reply carries the resolved SID and the canonical qualified name, whatever form the request used (§2.16).

A caller that looked up a bare name can therefore always tell which principal it got, compare two answers for identity, and record an unambiguous name in a log. Ambiguity is permitted in what a caller may ask; it is not permitted in what an authority answers.

Where no realm syntax exists, a canonical qualified name is indistinguishable from a bare one, and the SID alongside it is what carries the unambiguity. A client MUST NOT rely on the two being distinguishable, in either direction: it MUST NOT assume a returned name is unqualified, and MUST NOT treat one that is as a failure to canonicalise.

2.15.8 Shadowing #

Adding a source ahead of another in the search order changes which principal a bare name resolves to. The new principal has a different SID, so every security descriptor naming the old one stops applying to the person now signing in under that name.

This is inherent to a flat namespace and this chapter does not forbid it. An authority SHOULD detect it at logon — where the cost is one additional query per sign-in rather than one per lookup — and SHOULD record that a bare name resolved while another source could also have answered.

An authority MUST NOT fail a logon because it could not complete that check. A source being unreachable is not a reason to refuse a principal whose own source answered.

2.16 Lookup

Peios / Peios technical reference / PGSS / Logon

One request answers every question a name resolver asks.

2.16.1 Lookup #

msg_type = 0x0010. Client to authority.

FieldEncodingLimit
tagu32§2.14
key_typeu8§2.B
namestring256 bytes
sidlength-framed bytes (SID)68 bytes
unix_idu32
kindu8§2.B
fieldsu32§2.B

Exactly one of name, sid and unix_id is meaningful, selected by key_type. An encoder MUST leave the others empty or zero, and a decoder MUST ignore them.

2.16.1.1 key_type #

ValueNameAnswers
1Namegetpwnam, getgrnam
2SidRendering a security descriptor
3UnixIdgetpwuid, getgrgid

UnixId is a key even though no source is ever asked one. An authority converts the number to a source and a relative identifier by the range arithmetic it assigned, and asks that source by relative identifier or by SID.

A source therefore never receives an absolute number here, exactly as it never receives one during a logon. The property that made the authority the only possible answerer (§2.13) is the same property that keeps this side of it confined.

2.16.1.2 kind #

ValueName
0Any
1Principal
2Group

A request for Principal MUST NOT be answered with a group, and a request for Group MUST NOT be answered with a principal. Where the key matches only an object of the other kind, the outcome is NotFound (§2.18).

An authority MUST establish this for itself. Where the object came from a source, the authority MUST check the kind it received against the kind that was asked for, rather than relaying the source's answer and letting the client discover the mismatch.

Note

POSIX keeps users and groups in separate namespaces, so getpwnam and getgrnam can be asked the same string and expect different objects. Carrying the kind on the request is what lets both be served correctly without requiring an authority to forbid the collision.

2.16.1.3 fields #

A bitmask of the attributes the reply should carry. Identity is not among them: every successful reply carries the SID, the canonical qualified name, and the kind actually found, and a client cannot decline them.

BitNameValue encoding
0UNIX_IDu32
1PRIMARY_GROUPreference
2HOMEstring, 4096 bytes
3SHELLstring, 4096 bytes
4DISPLAY_NAMEstring, 256 bytes
5GROUPSarray of references, 128
6MEMBERSarray of references, 256
7CLAIMSarray of claim entries, 64
8ENABLEDu8
9LOGON_TYPESu32
10DESCRIPTIONstring, 1024 bytes

An authority MUST ignore a bit it does not implement, and MUST NOT report it (below). Claim entries use the claim attribute format PCDS §5.9 specifies.

LOGON_TYPES is a bitmask over the logon type values of §2.B: bit n set means the principal may be used for logon type n. Zero means the holder states nothing, not that nothing is permitted — a source that predates the field sends zero, and an authority that read that as "no logon permitted" would lock every principal on every existing system out at once.

An authority substituting a default for zero MUST NOT include Service in it. A principal that has never said how it may be used has certainly not said it may be used for a credential-free service logon (§2.19).

DESCRIPTION says what a group is for, in words for a person to read, on one line. A group with none is answered with an empty string: it has no description, which is an answer. A principal has none, and the field is withheld as Absent (below). An authority answers it for its own well-known groups as well as relaying a source's.

Note

Field bits are the one place in this chapter where an enumeration may grow without a version bump (§2.6). It is safe here, and only here, because the reply says which fields it is answering: an older authority ignoring a newer bit is reported as not implementing it, rather than silently omitting something a client believed it had asked for.

Requesting only what is wanted is not primarily a bandwidth measure — a getpwuid wants nearly every field anyway. It matters for the caller that resolves a dozen SIDs to render a security descriptor and wants only names, and it is the seam at which an authority can restrict individual fields without restricting the socket (§2.14).

2.16.2 LookupReply #

msg_type = 0x8010. Authority to client.

FieldEncodingLimit
tagu32§2.14
outcomeu8§2.B
sidlength-framed bytes (SID)68 bytes
qualified_namestring512 bytes
kind_foundu8§2.B
presentu32§2.B
withheldarray of withheld entries32
valuesarray of length-framed values32

Where outcome is not Found, everything after it MUST be empty or zero, and a client MUST NOT read it.

kind_found MUST be Principal or Group, never Any.

2.16.2.1 present, withheld and values #

values holds one length-framed value for each bit set in present, in ascending bit order. Each is length-framed so that a client can step over a value whose field it does not recognise.

Every bit set in the request is in exactly one of three states:

  • set in present — its value is in values;
  • listed in withheld — with a reason, below;
  • in neither — this authority does not implement the field.

The third state is a statement about the authority, not about the object. An authority that implements a field MUST place every request for it in one of the first two states, whatever happened underneath: a field an authority supports but could not obtain is Absent, Declined, Restricted or TooLarge, never silence. Reporting it as neither would tell the client the authority cannot answer that question at all, and a client is entitled to stop asking.

A withheld entry is a length-framed structure:

FieldEncodingLimit
fieldu32one bit, §2.B
reasonu8§2.B
ReasonMeaning
AbsentThe field has no value.
RestrictedThe caller may not have this field.
DeclinedThe source will not produce it.
TooLargeIt exists and exceeds one reply. Use Enumerate (§2.17).

Distinguishing these is the point of the structure. An empty member list and a source that refuses to enumerate members are the same bytes to a POSIX caller, and an administrator diagnosing a system needs to know which one happened.

2.16.2.2 References #

PRIMARY_GROUP, GROUPS and MEMBERS carry references rather than bare SIDs:

FieldEncodingLimit
sidlength-framed bytes (SID)68 bytes
namestring512 bytes
unix_idu32

An empty name means the authority has no name for that SID; a unix_id of zero means it has no number for it. Both are ordinary answers for a SID belonging to no source on this machine.

Zero is the only encoding of "no number". An authority MUST NOT substitute a POSIX identifier that a caller could mistake for a real one — a projection onto nobody is a rendering decision belonging to whatever produces a passwd record, and putting it on the wire destroys the distinction the client needs in order to make it.

Carrying the name is what keeps the round-trip discipline below intact. A reply of bare SIDs would make a single getgrnam into one request plus one per member.

2.16.3 Memberships #

GROUPS on a principal is the membership question, and it is the direction sources actually hold — the same one a logon uses. An authority MUST answer it whenever it can answer anything about the principal at all.

MEMBERS on a group is the reverse index, and it is not owed the same guarantee.

An authority MUST return MEMBERS as withheld, rather than as an error or an empty list, when it will not or cannot produce it:

  • Declined where the source will not enumerate the group's members.
  • TooLarge where the membership exceeds what one reply can carry.
  • Absent where the group is not one that has recorded members at all.

That last case is not a limitation. A group's membership may be recorded — held by a source, as a local group's is — or it may be a rule an authority applies when it derives a token. Nothing records who belongs to Everyone; an authority adds it to every token it mints. Groups reflecting how a principal signed in are further still from a recorded membership: they are properties of a logon rather than of a principal, and the same principal is in one at a console and not over a network.

An authority MUST report Absent for such a group rather than manufacturing a list, and MUST NOT report Declined, which would suggest an answer exists somewhere.

2.16.3.1 The BUILTIN groups #

The BUILTIN groups (S-1-5-32-…, such as Administrators, Users and Guests) are well-known, and the authority answers for them itself (§2.15), but their membership is recorded, not a rule. "jack is in Administrators" is a record a source keeps on this machine's behalf (PSPU §2.19), as it keeps a local group's.

For MEMBERS on a BUILTIN group, an authority MUST therefore gather the members each source records, from every source in its search order that is permitted to assert memberships outside its domain (PSPU §2.19), and MUST NOT report Absent. A source without that permission records none that a token would carry, and is not asked.

  • Where every such source answered and the list fits one reply, the members are present, in source order.
  • Where it will not fit, MEMBERS is withheld as TooLarge, and the members are walked with Enumerate (§2.17).
  • Where a source in the order could not be reached, or would not enumerate, MEMBERS is withheld as Declined. A list missing one source's members would look complete.

Note

This asymmetry is deliberate and follows the shape of the data. "Which groups is this principal in" is answered by every source cheaply, because it is how memberships are stored. "Who is in this group" needs an index in the other direction, over a set that may be the whole of an organisation — historically the reliable way to make a directory server fall over from a filesystem listing.

2.16.4 One round trip #

An authority MUST be able to answer each of the following in a single request:

Caller wantsRequest
A passwd recordLookup{key_type: UnixId or Name, kind: Principal, fields: UNIX_ID | PRIMARY_GROUP | HOME | SHELL | DISPLAY_NAME}
A group recordLookup{key_type: UnixId or Name, kind: Group, fields: UNIX_ID | MEMBERS}
A principal's groupsLookup{key_type: Name, kind: Principal, fields: GROUPS}
A name for a SIDLookup{key_type: Sid, kind: Any, fields: 0}

This is a design constraint on future revisions as much as a statement about this one. A name resolver is called from every process on the system, synchronously, and a field that cannot be fetched alongside the record it belongs to turns one lookup into several.

2.17 Enumeration

Peios / Peios technical reference / PGSS / Logon

Lookup answers about one object that a caller can already name. Enumeration answers when it cannot: walking every principal on the system, or every member of a group too large for one reply.

2.17.1 Enumerate #

msg_type = 0x0011. Client to authority.

FieldEncodingLimit
tagu32§2.14
kindu8§2.B
fieldsu32§2.B
of_key_typeu8§2.B
of_namestring256 bytes
of_sidlength-framed bytes (SID)68 bytes
of_unix_idu32
cursorlength-framed bytes256 bytes

kind MUST NOT be Any. A caller enumerating is filling a passwd or a group table, and the two are separate.

2.17.1.1 of #

Where of_key_type is zero, the request enumerates every object of kind that the authority knows.

Where it is non-zero, the named object MUST be a group, and the request enumerates that group's members. This is the continuation path for a MEMBERS field withheld as TooLarge (§2.16): the same answer, paged. An authority that withholds MEMBERS as TooLarge MUST serve this mode, since there is otherwise no way to obtain what it said existed.

A group's members are asked of the one source that holds the group. A BUILTIN group is no source's, and its members are walked across every source that may record them, in search order, as the whole table is (§2.16); a source in the order that did not contribute is reported in incomplete. A well-known group whose membership is a rule has no members to walk, and is NotFound here.

Note

Members are a field of Lookup for the common case and a mode of Enumerate for the large one, rather than a message of their own. The field keeps getgrnam to one round trip where the group is small, which is nearly always; this keeps the large case correct without a third request type or a partial member list, which would be worse than no list at all.

2.17.1.2 cursor #

Empty on the first request. On a continuation it MUST be the next returned by the immediately preceding reply.

A cursor is opaque. A client MUST NOT construct, parse or modify one, and MUST NOT present one to a different authority or after reconnecting.

An authority MUST reject a cursor it did not issue, or one it can no longer honour, with Malformed (§2.18). It MUST NOT silently restart the enumeration, and MUST NOT answer with an empty page and an empty next: the first hands the caller a second copy of the beginning under the impression it is continuing, and the second reports a truncated walk as a complete one.

2.17.2 EnumerateReply #

msg_type = 0x8011. Authority to client.

FieldEncodingLimit
tagu32§2.14
outcomeu8§2.B
entriesarray of entries256
nextlength-framed bytes256 bytes
incompletearray of strings32

An entry is a length-framed structure carrying the same fields as a successful LookupReply (§2.16), from sid through values.

An empty next means the enumeration is complete. A non-empty next means there is more, even if entries was empty — an authority may return a short or empty page while working through a source.

A client MUST continue until next is empty. A client MUST NOT infer completion from an empty page.

A client MUST NOT report an enumeration as complete when it stopped for any other reason. A transport failure, a timeout, or an outcome other than Found mid-walk is a truncated enumeration, and a client that presents it to its caller as the end of the list has manufactured an empty system out of an outage — the same error §2.18 forbids on a single lookup, at a scale where nothing records that it happened.

2.17.3 incomplete #

Each string names a source that did not contribute: because it declined to enumerate, or because it could not be reached.

An authority MUST list every such source. A client displaying an enumeration SHOULD say that it is partial, and MUST NOT discard the list without doing so.

Note

A short list that looks complete is the failure this field exists to prevent. An administrator reading an account listing has no way to tell a machine with four principals from a machine whose directory did not answer, and the difference decides whether they are looking at the whole picture.

2.17.4 No completeness guarantee #

An authority MUST NOT be required to enumerate.

A source may hold more principals than a reply, a page, or an administrator's patience can carry, and a source backed by a remote directory may be able to answer any single question while being quite unable to answer all of them. incomplete is the honest outcome, not a degraded one.

A caller MUST NOT treat enumeration as a way to test whether a principal exists. Lookup answers that, exactly, at any scale.

2.18 Outcomes

Peios / Peios technical reference / PGSS / Logon

Every reply on the identity channel carries an outcome.

ValueNameMeaning
1FoundThe request was answered.
2NotFoundNo such object, and every source that could have said so was asked.
3UnavailableA source that could have answered did not.
4RefusedThe caller may not make this request.
5MalformedThe request could not be understood.

Unlike the denial codes of §2.10, these are not a security boundary. NotFound reveals that a name is unused, which is what a name lookup is for.

An EnumerateReply carries an outcome on the same terms. An authority MUST NOT report Found on a page it could not produce.

2.18.1 NotFound and Unavailable #

If any source that could have answered was unavailable, the outcome is Unavailable — even if every source that did answer said no.

This is the most important rule in this part of the chapter, and it is stated normatively rather than left to implementations because it is invisible when wrong.

An authority is expected to cache. A cache that is told NotFound will store an absence, and if that absence was really a source being unreachable, it has memoised an outage as a fact. The account comes back when the source does; the cached answer does not. A principal is then unable to sign in, or a file is shown as owned by a number, for as long as the entry lives — with nothing in the system still recording that anything failed.

Unavailable is not cacheable, and that is the whole of the difference.

An authority MUST NOT report NotFound unless every source in the search order (§2.15) that could have answered was consulted and answered. A source that does not serve lookups at all is not such a source and does not need to be asked; a source that does, and could not be reached, is.

Note

The rule binds even where an authority does not cache, because its clients may. A name resolver is entitled to remember an answer for the length of a process, and it can only do that safely if the two are distinguishable.

2.18.2 Unavailable and the search order #

An authority MUST consult sources in the configured order and MUST stop at the first that answers, so an unreachable source later in the order does not make an answer Unavailable. It was never going to be asked.

An unreachable source earlier in the order does, even if a later one holds a matching name — because the earlier source is the one whose answer would have won.

Note

Returning the later source's principal in that situation would be worse than failing. The name would resolve to a different SID than it does when the system is healthy, so access decisions would be made against the wrong principal precisely while something is broken.

2.18.3 Refused #

Reserved. No field in this version is restricted by default, and an authority that restricts none will never send it.

It exists because the field mask (§2.16) is where restriction belongs, and a request refused in its entirety needs an answer that is not NotFound. An authority restricting a field MUST use the Restricted withheld reason instead, and MUST still answer the rest of the request.

Because it is reserved, Refused MUST NOT be used for anything else. A source that will not answer, a mode an authority has not implemented, or a question it cannot serve are not the caller lacking permission, and reporting them as Refused tells a client to stop asking on behalf of this caller when the answer would be the same for every caller. Those outcomes are Unavailable, Absent, or Malformed as the case requires.

An authority MUST NOT use NotFound in place of Refused or of a restricted field. Concealing a principal's existence from a caller that may not read their shell protects nothing — the SID is already in the file listing that prompted the lookup — and would make an authorization decision indistinguishable from an empty system.

2.18.4 Timing #

The prohibition in §2.10 on distinguishing an unknown principal from a bad credential does not apply here. There is no credential, and NotFound is an ordinary answer that this chapter states plainly.

An authority MUST NOT allow the presence of an answer in a cache to be observable to a caller that would be refused the answer itself. Where no field is restricted, nothing is refused, and the requirement is vacuous.

2.19 Service attestation

Peios / Peios technical reference / PGSS / Logon

msg_type = 0x0020. Client to authority, on /run/logon.sock. A complete request in one message; the authority answers with AccessGranted (§2.9) or AccessDenied (§2.10) and nothing else follows.

FieldEncodingLimit
identitystring128
servicestring256

A service has no human to prompt and no secret to present. This is the message by which the component that launches services obtains a token for one, and the whole of this section is about the conditions under which an authority may answer it.

2.19.1 A service identity MUST NOT have a credential #

An authority MUST NOT hold, store, or accept a credential for a service identity, and MUST NOT offer a credential exchange for one.

This is not a convenience. A machine that could authenticate its own services unattended would have to hold their secrets, so a credential for a service identity implies a store of service credentials on every machine that runs one — a structure that is read by anything which compromises the machine, and which grants whatever those identities were given. The requirement exists to make that structure impossible rather than to make it careful.

What replaces the credential is attestation: the authority satisfies itself about who is asking, and derives everything else.

2.19.2 Why this is not a LogonStart #

A LogonStart (§2.7) followed by no credentials would encode the same request. It is a separate message because the difference is structural rather than cosmetic.

Accepting a credential-free LogonStart creates a path through an authority's conversation handling on which zero credentials succeeds. The only thing then standing between an arbitrary principal and a credential-free token is the authorisation check below being correct — so a defect in that check is a complete authentication bypass rather than a defect.

With a distinct message, the credential-free path is reachable only by a message that carries no principal an authority would accept. The check remains necessary; it stops being the only thing.

An authority MUST reject a LogonStart that completes without a credential exchange for any identity it would accept here, and MUST reject a ServiceAttest naming an identity it would accept there.

2.19.3 Who may send it #

An authority MUST refuse a ServiceAttest unless both of the following hold of the connected peer, each established from the connection itself and never from the message:

  1. The peer is a principal the authority permits to attest — this standard does not fix which, and an implementation SHOULD keep the set as small as one.
  2. The peer is the process the platform designates as its service manager, established by a property of the connection that no other process can present.

Neither is sufficient alone, and an authority that checks only the first has a much weaker rule than it appears to. On a system where platform components share one identity — which is usual — the first condition alone admits every daemon on the machine, so compromising any one of them yields a token for any service identity the authority would mint.

An authority MUST answer a refusal with PermissionDenied.

Note

On Peios the two facts are that the peer's token names SYSTEM, and that the peer is PID 1. The second is sound there for a reason specific to PID 1 — it cannot exit and be replaced while the system runs, and the credentials are captured by the kernel when the connection is made — and that reasoning does not extend to any other process identifier.

2.19.4 What the authority may attest #

An authority MUST NOT mint a token under this message for a principal that has not been designated as usable for a service logon.

The designation is a property of the principal, held wherever that principal is held, and it is what keeps this message from being an oracle that yields a credential-free token for anybody on the machine. An authority that cannot determine whether a principal carries it MUST refuse with AccountRestricted.

Identities the authority defines for itself — the platform's own service identities, which no principal source holds and for which no credential could exist — are designated by construction.

2.19.4.1 Peios virtual service accounts #

On Peios, identity = "Service" (case-insensitive) selects a distinct virtual account whose user SID is the service SID derived from service. It uses the same policy evaluation and Service logon groups as other attestations, without LocalService or SYSTEM membership. The peer checks above precede this selection; no credential-bearing logon is added. LocalService and NetworkService remain available as shared account identities: their user SIDs are S-1-5-19 and S-1-5-20, respectively, while the derived per-service SID is a group. For Service, that per-service SID is both user and group. The derived groups are Everyone, Authenticated Users, Local and Service, plus the kernel’s session-specific logon SID. Local policy determines privileges and integrity for the resulting token.

A refusal here MUST NOT distinguish "no such principal" from "not designated for service logon", for the reason given in §2.10.

2.19.5 service, and what the authority cannot check #

service names the service being launched. The authority uses it to derive the per-service identifier it stamps on the token, which is what keeps two services running under the same identity distinguishable to an access decision.

An authority cannot verify it. Elsewhere in this standard a peer's claims are checked against the connection: an authority establishes who is calling rather than believing what it is told. That is not available here, because the process this request is about does not exist yet — there is no token to interrogate and nothing to compare the name against.

So service is taken on the peer's word. This is a genuine grant of trust and it is not removable: the service manager holds the service definitions, launches the process, and is the only component in a position to know which service it is launching. It is the reason the set of peers permitted to send this message is required to be as narrow as it is.

An authority MUST reject an empty or unusable service, with MalformedRequest. An empty name derives a well-formed identifier that names nothing and that every service making the same mistake would share.

2.19.6 The token #

Answered with AccessGranted (§2.9), carrying the token as a descriptor under the same rules as any other logon, and with the logon type recorded as Service.

The profile structure SHOULD be empty. A service has no home directory, no shell and no display name, and an authority that has nothing to say MUST leave the fields empty rather than invent values.

2.19.6.1 The token is weaker, and MUST be #

A token issued on attestation MUST NOT be usable to authenticate to another system on this authority's behalf.

The evidence behind it does not travel. A credential a principal source verified is evidence a second system could in principle have checked too; a local process's word is evidence on one machine only, and a token forwarded off the machine on the strength of it would let one system's service manager act as an arbitrary account on another.

Where a platform expresses this as a level on the token, the level an attested logon receives MUST be below the level a credentialled logon receives, and MUST bound everything derived from that token.

An authority MUST record, in whatever it records for a credentialled logon, that this logon was attested rather than authenticated. An audit trail that cannot tell the two apart cannot answer which authority vouched for a session.

2.20 Credential Change

Peios / Peios technical reference / PGSS / Logon

A principal changes their own credential by opening a conversation on /run/logon.sock with CredentialChangeStart. The authority asks for what it requires, in the rounds of §2.8, and ends the conversation with CredentialChanged or AccessDenied (§2.10).

client                                   authority
  |                                          |
  |-------- CredentialChangeStart ---------->|   principal = the peer
  |                                          |
  |<--------- CredentialRequest -------------|   the current credential
  |---------- CredentialResponse ----------->|
  |                                          |
  |<--------- CredentialRequest -------------|   the new one
  |---------- CredentialResponse ----------->|
  |                                          |
  |<-- CredentialChanged --------------------|   terminal
  |         or AccessDenied                  |

Adding a credential beside the ones a principal has, such as an SSH public key, or removing one, is the sibling conversation of §2.23.

Everything §2.3 says about the shape of a conversation applies unchanged: one conversation per connection, bounded rounds, exactly one terminal message. The client stays generic here as it does in a logon. It renders the prompts it is given and returns the answers, and it does not know what is being changed or how.

2.20.1 CredentialChangeStart #

msg_type = 0x0030. Client to authority. Opens the conversation; MUST be the first message.

FieldEncodingLimit
supported_credential_typeslength-framed bytes, one u8 per type32

supported_credential_types carries the meaning and the capability rule of the §2.7 field of the same name. It binds the authority, and a decoder MUST drop a value it does not recognise rather than refuse the message.

Unlike the §2.7 field, it is not optional. This message is newer than the rule that made that field optional, so there is no older client whose silence needs a default. An authority MUST refuse a CredentialChangeStart that ends before the field with MalformedRequest.

2.20.2 Whose credential #

The principal whose credential changes is the user of the connected peer's token, established from the socket as §2.4 requires. The message carries no identifier, and an authority MUST NOT change the credential of any other principal in this conversation.

The absence of an identifier is deliberate. A field naming a principal would let anything that can reach the socket ask to change anybody's credential, with only knowledge of that credential in the way. That is a guessing surface over every account the authority holds, open to every principal the socket admits. Taking the principal from the token confines each caller to their own account.

An authority MUST refuse, with AccountRestricted, a change for a principal it holds no changeable credential for. That includes a service identity (§2.19), a platform identity no source holds, and a principal that authenticates with no credential at all.

2.20.3 Proof of the current credential #

A token is not proof enough. An authority MUST establish, within the conversation, that the principal can present their current credential before it changes that credential, and MUST refuse with AuthenticationFailed where they cannot.

A token shows that someone signed in. It does not show that the person at the keyboard now is that person. An unattended terminal holds a token too, and a credential changed from one cannot be recovered by signing in again, because the credential needed to sign in is the one that changed.

AuthenticationFailed keeps the wording rule of §2.10 here: reason MUST NOT say what was wrong with the credential. The caller cannot use the distinction to learn about another account, since only their own is reachable, but a reason that varied would still tell a guesser which part of a guess was right.

2.20.4 The new credential #

What the authority asks for, and what it accepts, is its own policy. An authority that refuses a proposed credential MAY say why in a message of Error severity (§2.8) and ask again, within its round limit. An authority that gives up MUST end the conversation with ConversationLimit.

The new credential is credential material like any other. It is collected and handled under §2.12, and the client neither knows nor cares that it is new.

2.20.5 CredentialChanged #

msg_type = 0x8030. Authority to client. The successful terminal message; nothing follows it.

It has no fields and no ancillary data. A change mints no token and creates no session: the caller already holds a session, and the change leaves it as it was. A client MUST NOT expect a descriptor with this message, and MUST close one if one arrives.

An authority MUST NOT send CredentialChanged until the new credential is the one a subsequent logon will be tested against. A principal told about a change that is not yet durable can lose it, and finds out only when the old credential works and the new one does not.

What a change does to sessions already established is the authority's policy. This chapter specifies none.

2.20.6 Access to the socket #

Every principal permitted to change their own credential needs connect access to /run/logon.sock (§2.5), and most of them may not originate a logon. The socket's descriptor therefore admits both kinds of peer and cannot tell them apart. An authority whose descriptor admits a peer that may not originate logons MUST decide, from each peer's identity (§2.4), whether that peer may originate one. It MUST NOT treat reaching the socket as that permission.

The two kinds of peer SHOULD NOT share one bound on concurrent conversations (§2.5). A principal who can reach the socket to change a password can also open connections and hold them open, and a shared bound they could exhaust would stop the console signing anyone in.

2.20.7 An authority that does not offer it #

An authority MAY decline to offer credential change. One that declines MUST refuse CredentialChangeStart with PermissionDenied, and is conforming.

2.21 Conformance

Peios / Peios technical reference / PGSS / Logon

A conforming implementation MUST satisfy every requirement in this chapter. This section collects them by role.

2.21.1 Authority obligations #

An implementation claiming to be a PGSS Logon authority MUST satisfy all of the following.

2.21.1.1 Channel #

  1. Listen on /run/logon.sock as a SOCK_STREAM Unix domain socket (§2.5).
  2. Control access to it with a security descriptor, not with process integrity and not with POSIX permission bits (§2.4).
  3. Serve conversations concurrently, so that one stalled logon does not block others (§2.5).
  4. Support SCM_RIGHTS on the channel (§2.9).

2.21.1.2 Framing #

  1. Reject any message whose magic is not PGSL, as a whole-connection failure (§2.6).
  2. Reject any message whose version it does not implement, answering UnsupportedVersion where it can still encode a denial (§2.6).
  3. Reject any message declaring more than 65536 bytes, or fewer than a header, without reading the body (§2.6).
  4. Skip to a structure's declared end after reading known fields, rather than assuming its length, and substitute a documented default for an optional trailing field a peer did not write (§2.6).

2.21.1.3 Conversation #

  1. Require LogonStart, CredentialChangeStart, CredentialEnrollStart, SessionEnd or SessionEndQuery as the first message, and reject a conversation opening otherwise (§2.3).
  2. Send exactly one terminal message, nothing after it, and close the connection (§2.3).
  3. Bound the number of rounds, the time spent awaiting an answer, and the time a conversation may remain open, and terminate with ConversationLimit on exhausting any of them (§2.3, §2.5). A bound reached MUST produce a terminal message; closing the connection silently is what the code exists to prevent.
  4. Reject a CredentialResponse it did not solicit (§2.3).
  5. Match answers to prompts by credential_ref, never by position, and treat a missing answer as a failed logon rather than as grounds to tear down the connection (§2.8).

2.21.1.4 Trust #

  1. Establish peer identity from the connected socket's peer token, never from a message body, and never via SO_PEERCRED (§2.4).
  2. Treat logon_type as a proposal and constrain it against the verified peer (§2.4).
  3. Treat identifier, tty and remote_host as unverified claims (§2.4).

2.21.1.5 Derivation #

  1. Perform derivation itself, and never accept a token, privilege set or integrity level from another party (§2.1).
  2. Never fork or exec on the client's behalf; never learn about terminals, environments or session leadership (§2.1).

2.21.1.6 Prompting #

  1. Never send a prompt for a credential type absent from the client's supported_credential_types, and deny the logon instead if it cannot proceed within them (§2.8).
  2. Ensure credential_ref is unique among the prompts of one request (§2.8).

2.21.1.7 Result #

  1. Transfer the token as a file descriptor in the ancillary data of AccessGranted, never by name (§2.9).
  2. Never distinguish an unknown principal from a bad credential — by denial code, by reason text, or by timing (§2.10, §2.12).
  3. Send home and shell as absolute paths, or empty, whatever their provenance; an authority with no value for a profile field MUST leave it empty rather than invent one (§2.9).

2.21.1.8 Credential handling #

  1. Erase credential material, and every buffer it was encoded and decoded through — including an allocation abandoned by a buffer that grew — before that memory is released (§2.12).
  2. Never write credential material to a log, audit record, error message or diagnostic (§2.12).

2.21.1.9 Identity lookup #

  1. Listen on /run/ident.sock as well as /run/logon.sock, as a SOCK_STREAM Unix domain socket (§2.14).
  2. Grant connect access to that socket, by security descriptor, to every principal that runs ordinary programs (§2.14).
  3. Refuse a message of one socket's range received on the other (§2.14).
  4. Echo each request's tag, and never interpret it otherwise (§2.14).
  5. Serve requests concurrently, and never let a slow answer on one connection delay another (§2.14).
  6. Bound the time spent answering a request, and answer Unavailable rather than waiting indefinitely (§2.14).
  7. Ignore ancillary data received on the identity socket (§2.14).

2.21.1.10 Resolution #

  1. Interpret names itself, and never require a client to parse, qualify or case-fold one (§2.15).
  2. Compare names ASCII case-insensitively, establishing the comparison itself rather than delegating it to a source (§2.15).
  3. Refuse a name containing a reserved character, a byte outside 0x20–0x7e, or a leading or trailing space — whether created locally, received in a request, asserted by a source, or carried alongside a SID in a reference (§2.15).
  4. Resolve bare names in a configured order, never in registration order, stopping at the first source that answers (§2.15).
  5. Carry the resolved SID and the canonical qualified name on every successful reply, whatever form the request used (§2.15).

2.21.1.11 Answers #

  1. Never answer a Principal request with a group, or a Group request with a principal, and establish the kind itself rather than relaying a source's (§2.16).
  2. Ignore a field bit it does not implement, and never report it as present (§2.16).
  3. Report each requested field it implements as present or as withheld with a reason, never as neither (§2.16).
  4. Withhold MEMBERS with a reason rather than returning a partial or empty list, where it will not or cannot produce it (§2.16).
  5. Report Absent rather than Declined for a group whose membership is a rule rather than a record, and gather a BUILTIN group's recorded members from the sources rather than reporting it Absent (§2.16).
  6. Encode "no POSIX identifier" as zero, and never substitute a number a caller could mistake for a real one (§2.16).
  7. Answer a passwd record, a group record, a principal's memberships, or a name for a SID, each in one request (§2.16).
  8. Never report NotFound unless every source in the search order that could have answered was consulted and answered (§2.18).
  9. Report Unavailable where a source earlier in the search order could not be reached (§2.18).
  10. Never report NotFound in place of Refused or of a Restricted field, and never report Refused for anything other than a caller that may not make the request (§2.18).
  11. List every source that did not contribute to an enumeration, and never report Found on a page it could not produce (§2.17, §2.18).
  12. Reject a cursor it did not issue, or can no longer honour, with Malformed — never by restarting the walk and never by reporting it complete (§2.17).
  13. Serve member enumeration where it withholds MEMBERS as TooLarge (§2.17).

2.21.1.12 Service attestation #

An authority that implements ServiceAttest MUST satisfy all of the following. An authority that does not implement it MUST refuse the message, and is conforming.

  1. Never hold, store, or accept a credential for a service identity, and never offer a credential exchange for one (§2.19).
  2. Refuse a ServiceAttest unless both the peer's principal and the peer's being the platform's service manager are established from the connection, and never from the message (§2.19).
  3. Answer a peer that satisfies neither condition, or only one, with PermissionDenied (§2.19).
  4. Never mint a token under this message for a principal that has not been designated as usable for a service logon, and refuse with AccountRestricted where it cannot determine whether a principal carries that designation (§2.19).
  5. Never let a refusal distinguish "no such principal" from "not designated for service logon" (§2.19, §2.10).
  6. Reject an empty or unusable service with MalformedRequest (§2.19).
  7. Record the logon type as Service, and leave every profile field empty rather than inventing a value (§2.19, §2.9).
  8. Issue a token that cannot be used to authenticate to another system on the authority's behalf, at a level below the one a credentialled logon receives and bounding everything derived from it (§2.19).
  9. Record that the logon was attested rather than authenticated, wherever it records what vouched for a credentialled logon (§2.19).
  10. Reject a LogonStart that would complete with no credential exchange for an identity it accepts under ServiceAttest, and reject a ServiceAttest naming an identity it accepts under LogonStart (§2.19).

2.21.1.13 Logon type restrictions #

  1. Refuse a logon whose type the principal's holder does not permit, with AccountRestricted, after authentication rather than instead of it (§2.16, §2.10).
  2. Read an unstated LOGON_TYPES as its own default rather than as "nothing permitted", and never include Service in that default (§2.16, §2.19).
  3. Never widen what a principal's holder permits. Local policy may narrow it further; nothing may add to it (§2.16).
  4. Refuse a logon_type it does not recognise, with MalformedRequest (§2.7, §2.10).

2.21.1.14 Credential change #

An authority that implements CredentialChangeStart MUST satisfy all of the following. An authority that does not implement it MUST refuse the message with PermissionDenied, and is conforming. Obligations 10 to 13, 19, 20, 24 and 25 bind a change conversation as they bind a logon.

  1. Change only the credential of the principal the connected peer's token names, established from the socket and never from a message (§2.20, §2.4).
  2. Establish, within the conversation, that the principal can present their current credential before changing it, and refuse with AuthenticationFailed where they cannot (§2.20).
  3. Refuse with AccountRestricted a principal it holds no changeable credential for, including every service identity (§2.20, §2.19).
  4. Send CredentialChanged only once the new credential is the one a subsequent logon will be tested against (§2.20).
  5. Mint no token and create no session for a change, and send no descriptor with CredentialChanged (§2.20).
  6. End with ConversationLimit a change it gives up on for want of an acceptable new credential (§2.20).
  7. Refuse a CredentialChangeStart that ends before supported_credential_types with MalformedRequest (§2.20).
  8. Where the socket's descriptor admits peers that may not originate logons, decide from each peer's identity whether it may originate one, and never treat reaching the socket as that permission (§2.20, §2.4).

2.21.1.15 Ending a session #

An authority that implements SessionEnd and SessionEndQuery MUST satisfy all of the following. An authority that does not implement them MUST refuse both with PermissionDenied, and is conforming. Obligations 5 to 8, 10 and 14 bind these requests as they bind a logon.

  1. Never accept SessionEndQuery as SessionEnd or the reverse, and change nothing in answering SessionEndQuery (§2.22).
  2. Answer SessionEndQuery with SessionEndAllowed exactly where a SessionEnd from the same peer would be permitted, and otherwise with the denial it would draw (§2.22).
  3. Refuse, with PermissionDenied, a peer whose token is restricted, the SYSTEM and Anonymous sessions the kernel creates, and every session of logon type Service, whoever asks (§2.22).
  4. Permit a caller to end a session whose user is the caller, of a logon type a person has, where the caller is a principal of an issued domain (§2.22).
  5. Decide any other request by an access check of the caller's token against a descriptor local policy states — SYSTEM and Administrators alone where it states none, and nobody where it states one that cannot be used (§2.22).
  6. Answer NoSuchSession only to a caller who may end other principals' sessions, and PermissionDenied to any other where the session does not exist (§2.22, §2.10).
  7. End every process whose primary token belongs to the session — asking first, then, after a grace period of at most 10 seconds, without asking — and never a process only because one of its threads impersonates a token of the session (§2.22).
  8. Address each signal by a reference that cannot come to name a different process, never by a process identifier looked up again (§2.22).
  9. Look again after each round, bound the number of rounds, and answer within 30 seconds (§2.22).
  10. Count in remaining every process it could not examine or end, and never fail the request for one (§2.22).
  11. Hold no token of the session longer than it takes to read which session it belongs to, and never end its own process (§2.22).
  12. Where the session is the one the connected peer's token belongs to, send SessionEnded before signalling anything, carrying what it found to end and a remaining of zero (§2.22).
  13. Start nothing in ending a session (§2.1, §2.22).

2.21.1.16 Credential enrolment #

An authority that implements CredentialEnrollStart MUST satisfy all of the following. An authority that does not implement it MUST refuse the message with PermissionDenied, and is conforming. Obligations 10 to 13, 19, 20, 24, 25, 68, 69 and 72 bind an enrolment as they bind a credential change.

  1. Add or remove only the credentials of the principal the connected peer's token names, established from the socket and never from a message (§2.23, §2.4).
  2. For both Add and Remove, establish within the conversation that the principal can present their current credential before changing anything, and refuse with AuthenticationFailed where they cannot (§2.23).
  3. Refuse with AccountRestricted a principal it holds no credential for that it can ask for as proof, including every service identity (§2.23, §2.19).
  4. Refuse an action or credential_type it does not recognise with MalformedRequest, a credential_type it does not enrol with PermissionDenied, and a CredentialEnrollStart that ends before material with MalformedRequest (§2.23).
  5. Refuse material it will not accept with CredentialRejected, check it again when applying the change, and never send CredentialRejected for a failed proof or in answer to any other message (§2.23, §2.B).
  6. Leave unchanged which credentials the principal may sign in with (§2.23).

2.21.2 Client obligations #

There are four client roles, and they are independent. A program may be any of them, several, or none: a logon originator never looks a principal up; a name resolver does the reverse.

An implementation originating logons MUST satisfy obligations 1 to 20. An implementation performing identity lookup MUST satisfy 21 to 27. An implementation changing its own principal's credential MUST satisfy obligations 2 to 12 and 18 to 20, reading CredentialChangeStart wherever they say LogonStart, and 28 and 29. An implementation adding or removing its own principal's credentials MUST satisfy the same, reading CredentialEnrollStart for CredentialChangeStart, and 34 and 35. An implementation ending logon sessions MUST satisfy obligations 4 to 7 and 30 to 33.

2.21.2.1 Conversation #

  1. Send exactly one LogonStart, as the first message (§2.3).
  2. Answer each CredentialRequest with exactly one CredentialResponse (§2.3).
  3. Never send a CredentialResponse that was not solicited (§2.3).
  4. Treat a connection that closes without a terminal message as a failed logon, and not retry automatically (§2.3).

2.21.2.2 Framing #

  1. Reject any message whose magic is not PGSL (§2.6).
  2. Reject any message whose version it does not implement (§2.6).
  3. Skip to a structure's declared end after reading known fields (§2.6).

2.21.2.3 Capabilities #

  1. Never declare in supported_credential_types a credential type it cannot render (§2.7). Declaring fewer than it can render is permitted, and an empty list is how a client asks for a logon requiring no interaction.
  2. Fail the conversation, rather than guess, if it receives a prompt it cannot render (§2.8).

2.21.2.4 Rendering #

  1. Display received messages, in order, before the prompts of the same request (§2.8).
  2. Not interpret or branch on message text, credential_name, or reason (§2.8, §2.10).
  3. Echo credential_ref back unchanged (§2.8).

2.21.2.5 Result #

  1. Read the token descriptor from the ancillary data of AccessGranted, and treat an AccessGranted without one as a failed logon (§2.9).
  2. Close the descriptor if it does not install the token (§2.9).
  3. Have a fallback for every profile field, and treat an empty field, and an absent profile, identically and not as an error (§2.9).
  4. Never execute a relative shell, nor resolve a relative home against its own working directory (§2.9). A shell containing no separator is relative, and MUST NOT be resolved against a search path.
  5. Never treat profile as a directory lookup, nor cache it as an answer about any principal other than the one who just authenticated (§2.9).

2.21.2.6 Credential handling #

  1. Collect a credential only where it can establish that its collection method meets the type's requirements, and never collect a Password with echo enabled (§2.12).
  2. Never silently truncate an answer to fit its own buffer (§2.12).
  3. Erase credential material, and every buffer it was collected and encoded through, before that memory is released, and never write it to a log, error message or diagnostic (§2.12).

2.21.2.7 Identity lookup #

  1. Forward a name unchanged, and never parse, split, qualify or case-fold one (§2.15).
  2. Never reuse a tag while a request bearing it is outstanding, and never assume replies arrive in request order (§2.14).
  3. Treat a cursor as opaque: never construct, parse or modify one, nor present one to a different authority or across a reconnection (§2.17).
  4. Continue an enumeration until next is empty, never infer completion from an empty page, and never present a walk it abandoned for any other reason as a complete one (§2.17).
  5. Surface an enumeration's incomplete list rather than discarding it (§2.17).
  6. Distinguish NotFound from Unavailable, and never cache the second as though it were the first (§2.18).
  7. Never use enumeration to test whether a principal exists (§2.17).

Obligations 24 and 26 bind a client that caches for the lifetime of a single process just as they bind an authority. A resolver that remembers "no such user" through an outage will keep reporting it after the outage ends, and one that reports a failed walk as an empty system does the same thing to every principal at once.

2.21.2.8 Credential change #

  1. Send exactly one CredentialChangeStart, as the first message, and name no principal in it — there is no field for one (§2.20).
  2. Treat CredentialChanged as the only successful outcome, expect no token with it, and close any descriptor that arrives with it (§2.20).

2.21.2.9 Ending a session #

  1. Send exactly one SessionEnd or SessionEndQuery, as the only message, and use SessionEndQuery — never SessionEnd — to learn whether ending a session would be permitted (§2.22).
  2. Wait up to 60 seconds for the answer to SessionEnd before treating it as unanswered, and then not assume either outcome (§2.22).
  3. Treat a non-zero remaining as a session that may still exist, and never read the counts as an outcome where the session was the caller's own (§2.22).
  4. Treat a denial code it does not recognise as a refusal, not as an unreadable answer (§2.B).

2.21.2.10 Credential enrolment #

  1. Send exactly one CredentialEnrollStart, as the first message, with every field, and name no principal in it (§2.23).
  2. Tell CredentialRejected (the material) apart from AuthenticationFailed (the proof) where it reports a refusal, and treat a denial code it does not recognise as a refusal (§2.23, §2.B).

2.21.3 What a client is not required to do #

A client is not required to understand what any credential type means, what any message says, or why a logon was denied. It renders what it is given and returns what it collects.

That is the property the whole conversational shape exists to produce, and a client that starts reasoning about the content of prompts has given it up — it will need changing the next time an authority's policy does.

2.22 Ending a Session

Peios / Peios technical reference / PGSS / Logon

A caller asks the authority to end a logon session with SessionEnd, and asks whether it may with SessionEndQuery. Each is a whole conversation in one message, on /run/logon.sock: the client sends it first and alone, and the authority answers with one terminal message.

client                                   authority
  |                                          |
  |------------ SessionEndQuery ------------>|   may I?
  |<-- SessionEndAllowed | AccessDenied -----|   nothing changed
  |                                          |

client                                   authority
  |                                          |
  |-------------- SessionEnd --------------->|   end it
  |                                          |   (ends its processes)
  |<---- SessionEnded | AccessDenied --------|   terminal

§2.3's rules hold for both: one per connection, the request as the first and only client message, exactly one terminal message, and the connection closed after it.

2.22.1 Why the authority #

The kernel has no call that ends a logon session. A session lasts as long as a token of it does, and is destroyed when the last reference goes (Kernel TRM §3.2.7). Ending a session is therefore ending the processes that run in it, after which the kernel does the rest.

Any sufficiently privileged program could do that walk. It belongs to the authority for two reasons.

One decision, one record. The authority created the session and is the component the standard already trusts to decide who may do what with logons. A desktop's task manager, a principal manager and a command-line tool asking it get the same answer, under one policy, and leave one record of what was ended at whose request.

It can finish. A process in the session may be protected, or carry a descriptor that does not let an administrator end it. A program doing the walk itself can stop halfway and leave half a session. The authority, as the most trusted process outside the kernel, can end what an administrator's tools cannot.

2.22.2 SessionEndQuery #

msg_type = 0x0041. Client to authority. The whole request.

FieldEncodingLimit
logon_session_idu64—

logon_session_id is the session's identifier: the auth_id every token of the session carries.

The authority answers with SessionEndAllowed where a SessionEnd for the same session from the same peer would be permitted now, and otherwise with AccessDenied carrying the denial that SessionEnd would draw. It MUST NOT signal any process, or change anything else, in answering.

It exists so that a program can decide whether to offer the act at all. A window that shows a person a button that can only fail has told them something false.

2.22.3 SessionEnd #

msg_type = 0x0040. Client to authority. The whole request.

FieldEncodingLimit
logon_session_idu64—

The authority ends the session's processes as §2.22 describes below and answers with SessionEnded, or refuses with AccessDenied.

2.22.4 Why the question is a separate message #

The two requests have the same body and differ only in msg_type. They could have been one message with a flag saying "only ask". They are not, for the reason ServiceAttest is not a LogonStart (§2.19): a flag puts the path that ends a session on the same dispatch as the path that must not, separated by one bit. With two messages, a probe cannot end a session by any corruption or confusion of a field — only by being a different message.

An authority MUST NOT accept either message as the other.

2.22.5 Who may end a session #

The caller is the user of the connected peer's token, established from the socket as §2.4 requires. An authority MUST refuse, with PermissionDenied, a peer whose token is restricted: a restricted token does not speak for its user's whole authority.

Unlike CredentialChangeStart (§2.20), these messages name their subject. They have to: the usual caller is an administrator, and the session is somebody else's. What the name buys is decided entirely by who is asking.

An authority MUST apply the following, in this order.

  1. Never the kernel's sessions, never a service's. An authority MUST refuse, with PermissionDenied, the session the kernel creates for SYSTEM, the one it creates for Anonymous, and every session whose logon type is Service — whoever asks, the session's own user and SYSTEM included. Every process on the machine descends from the first and depends on it. A service is stopped through the service manager, which knows how to stop it and whether to start it again; signing it out from under that manager does neither.
  2. Your own session. An authority MUST permit a caller to end a session whose user is the caller, whose logon type is one a person has — one of those §2.B lists, other than Service — and whose user is a principal of an issued domain (S-1-5-21-A-B-C-RID). No grant is needed: signing yourself out is not a privilege. A well-known identity is not a person signing out, whatever its session's type.
  3. Anybody else's. Otherwise, an authority MUST decide by an access check of the caller's token, for a right that means "end a session", against a security descriptor its local policy states.
    • Where local policy states no descriptor, the authority MUST permit SYSTEM and Administrators and nobody else.
    • Where it states one that cannot be used — the wrong type, empty, unreadable, or not a descriptor — the authority MUST permit nobody, and SHOULD record why. Falling back to the default would grant more than the site wrote to anybody the site meant to exclude, and nobody would notice; refusing everybody is noticed at once.
  4. No such session. Where the session does not exist, an authority MUST answer NoSuchSession to a caller rule 3 permits, and PermissionDenied to any other.

The last rule is about disclosure. Which sessions exist — who is signed in, and since when — is not something every principal may list, and a NoSuchSession that differed from a refusal would let anybody who can reach the socket enumerate sessions by number.

PermissionDenied and NoSuchSession are the only denials these messages draw for a decision. MalformedRequest, UnsupportedVersion and Internal keep their usual meanings (§2.10).

Note

On Peios the descriptor is the REG_SZ value SessionEndSecurity on Machine\Generic\Authn\Policy, in SDDL, read on every request. The right is 0x1, and every generic right maps to it. With the value absent the descriptor is O:SYG:SYD:(A;;0x1;;;SY)(A;;0x1;;;BA). The kernel's sessions are 999 (SYSTEM) and 998 (Anonymous).

2.22.6 What the authority ends #

An authority MUST end every process whose primary token belongs to the session, by asking each to terminate, giving it a grace period, and then terminating whatever is left without asking.

It MUST NOT signal a process because one of its threads is impersonating a token of the session. A service that impersonates a person to answer one request is not in that person's session, and ending it would end the service for everybody.

It MUST address each signal to the process whose token it examined, by a reference that cannot come to name a different process — never by a process identifier looked up again, which may have been reused in the meantime.

A process can start another during the grace period, and the new one holds the session too. So after each round an authority MUST look again, and end what it finds in a further round. It MUST bound the number of rounds, and MUST bound the grace period. Both bounds are implementation-defined, but the grace period MUST NOT exceed 10 seconds, and the whole request MUST be answered within 30 seconds — half of the 60 a client waits (§2.21), so that a slow authority and a patient client never meet at the same instant.

A process the authority cannot examine, or cannot signal, is not a failure of the request. It is counted in remaining, and the authority carries on with the rest.

An authority MUST NOT end its own process, and MUST NOT hold a token of the session for longer than it takes to read which session the token belongs to. Each token held is a reference that keeps the session alive.

An authority SHOULD record each SessionEnd it acts on — who asked, which session and whose it was, and the counts it answered — wherever it records logons.

This is the one thing the authority does to processes. It starts none, and §2.1's prohibition stands.

Note

Mainline's authority finds the session in /sys/kernel/security/kacs/sessions, walks /proc, opens each process by pidfd_open and reads its primary token through kacs_open_process_token on that pidfd, so the signal it later sends by pidfd_send_signal reaches the process whose token was read. It sends SIGTERM, waits up to 5 seconds for everything signalled to exit, sends SIGKILL to what is left, waits up to 1 second, and walks again — at most three rounds, so at most about 18 seconds.

2.22.7 SessionEnded #

msg_type = 0x8040. Authority to client. The successful terminal message of SessionEnd; nothing follows it.

FieldEncodingLimit
endedu32—
remainingu32—

ended is the number of processes of the session the authority signalled that its last look no longer found.

remaining is the number of processes its last look did find still holding the session, plus those it could not examine. Not zero means the session may still exist, and a client SHOULD say so rather than report the person signed out.

Both fields are mandatory. The message is newer than the rule that lets a trailing field be absent, so there is no older authority whose silence needs a default.

The counts are of processes, not of references. A session outlives its processes wherever a reference the authority cannot see survives them — see below — and such a reference is counted nowhere.

2.22.8 SessionEndAllowed #

msg_type = 0x8041. Authority to client. The successful terminal message of SessionEndQuery; nothing follows it. It has no fields.

2.22.9 When the caller is in the session #

A caller may end the session it is running in, and a person signing themselves out usually does. Ending it ends the caller, and the authority cannot answer a connection whose other end is gone.

So where the session being ended is the one the connected peer's token belongs to, an authority MUST send SessionEnded before it signals anything. Its ended is then the number of processes it found to end, and remaining is zero: the counts say what the authority is about to do, not what it achieved. It then ends the session exactly as it would any other.

A client in that position MUST NOT read the counts as an outcome, and SHOULD expect to be ended shortly after it reads them.

The connection is itself a reference to the caller's token, held by the authority's end of the socket, so the session is not destroyed until the authority has closed it.

2.22.10 What ending a session cannot reach #

The kernel destroys a session when its last reference goes, and some references are not processes running in the session:

  • A token descriptor held elsewhere. A token can be passed between processes as a descriptor (Kernel TRM §3.2.7). A process outside the session holding one keeps the session alive after every process in it has ended, and nothing here finds it.
  • An impersonating thread. By the rule above, a thread of another process impersonating a token of the session is not ended. The session lasts until that thread reverts.
  • A process not yet reaped. A process that has exited still holds its token until its parent reaps it. It counts in remaining, and ending its parent, where that is in the session, usually releases it.

An authority is not required to find these, and a client MUST NOT assume that remaining of zero means the session is gone.

2.22.11 Access to the socket #

Every principal permitted to end its own session needs connect access to /run/logon.sock. That is the same population §2.20 admits, and as there, reaching the socket permits nothing by itself.

2.22.12 An authority that does not offer it #

An authority MAY decline to offer ending sessions. One that declines MUST refuse both SessionEnd and SessionEndQuery with PermissionDenied, and is conforming.

2.23 Credential Enrolment

Peios / Peios technical reference / PGSS / Logon

A principal adds a credential of their own beside the ones they have, or removes one, by opening a conversation on /run/logon.sock with CredentialEnrollStart. The only credential this section defines for enrolment is an SSH public key (§2.D). The authority asks for the principal's current credential, in the rounds of §2.8, and ends the conversation with CredentialChanged or AccessDenied (§2.10).

client                                   authority
  |                                          |
  |-------- CredentialEnrollStart ---------->|   principal = the peer
  |                                          |   material: the key, or which
  |<--------- CredentialRequest -------------|   the current credential
  |---------- CredentialResponse ----------->|
  |                                          |
  |<-- CredentialChanged --------------------|   terminal
  |         or AccessDenied                  |

It is the sibling of a credential change (§2.20), and everything that section says about the shape of the conversation, whose credential is affected, and the terminal holds here unless this section says otherwise. The difference is where the material travels: a change collects the new credential in a round, while an enrolment carries it in the opening message. That is possible because the material is public: an SSH public key, or the fingerprint of one.

2.23.1 CredentialEnrollStart #

msg_type = 0x0031. Client to authority. Opens the conversation; MUST be the first message.

FieldEncodingLimit
supported_credential_typeslength-framed bytes, one u8 per type32
actionu8§2.B
credential_typeu8§2.B
materialstring16384

Every field is mandatory. The message is newer than the rule that lets a trailing field be absent, so there is no older client whose silence needs a default, and a truncated message read with one could perform an action nobody asked for. An authority MUST refuse a CredentialEnrollStart that ends before material with MalformedRequest.

2.23.1.1 supported_credential_types #

The credential types the client can render, for the proof the authority will ask for. It carries the meaning and the capability rule of the §2.7 field of the same name, and a decoder MUST drop a value it does not recognise rather than refuse the message.

2.23.1.2 action #

1, Add: enrol material as a new credential of the principal. 2, Remove: remove the credential material identifies. The values are instructions rather than statements of capability, so a decoder MUST refuse one it does not recognise with MalformedRequest.

2.23.1.3 credential_type #

The kind of credential being added or removed, from the credential types of §2.B. Only SshPublicKey is defined for enrolment. An authority MUST refuse any other with PermissionDenied.

2.23.1.4 material #

For Add of an SshPublicKey, one line of an OpenSSH public key file, such as ~/.ssh/id_ed25519.pub: the key type, the base64 key and an optional comment. The comment MAY become the key's label.

For Remove of an SshPublicKey, the key's fingerprint as ssh-keygen -l writes it: SHA256: and the unpadded base64 of the SHA-256 of the key blob (§2.D).

2.23.2 Whose credential #

The principal whose credentials change is the user of the connected peer's token, as in §2.20, and for the same reason: a field naming a principal would let anything that reaches the socket ask to add a key to anybody's account. The message carries no identifier, and an authority MUST NOT enrol a credential for, or remove one from, any other principal in this conversation.

An authority MUST refuse, with AccountRestricted, a principal for whom it holds no credential it can ask for as proof (below). That includes a service identity (§2.19), a platform identity no source holds, a principal that authenticates with no credential at all, and a principal that authenticates only with the kind of credential being enrolled. The reason SHOULD tell the principal that an administrator can make the change for them.

2.23.3 Proof of the current credential #

Both actions require it. An authority MUST establish, within the conversation, that the principal can present their current credential before it adds or removes anything, and MUST refuse with AuthenticationFailed where they cannot. The rule and its wording requirement are §2.20's.

A token shows that someone signed in, not that the person at the keyboard now is that person. A key added from an unattended terminal is a way back into the account for whoever added it, and outlives every session; a key removed from one locks its owner out of every machine that trusts it. Neither can be undone by signing in again.

The proof is a credential the principal already has and that is not the one being enrolled. A principal who signs in only with SSH keys cannot prove themselves with a key over this socket, since there is no transport to bind the proof to (§2.D); such a principal is refused as above.

2.23.4 Checking the material #

The authority decides what it accepts: which key types and sizes, how many keys a principal may hold, and whether a key may be held twice. It SHOULD check the material before it asks for the current credential, so that a principal is not asked for a password to be told their key is unreadable, and it MUST check it again when it applies the change, since the principal's credentials may have changed between the two.

An authority MUST refuse material it will not accept with CredentialRejected, and SHOULD say why in reason: unreadable, an unsupported type, a key the principal already has, one more than the principal may have, or, for Remove, no key of the principal's with that fingerprint. CredentialRejected answers only this message (§2.B). It MUST NOT be used where the proof failed: that is AuthenticationFailed, and the two are separate so that a client can tell "your key" from "your password".

Enrolment does not change which credentials a principal may sign in with. Whether an enrolled key is accepted at sign-in is the authority's policy for that principal, as it is for a key an administrator adds.

2.23.5 CredentialChanged #

The successful terminal message is §2.20's CredentialChanged, with every rule that section states for it: no fields and no descriptor, and not sent until the change is the one a subsequent logon will be tested against. For Remove, that means the key no longer authenticates the principal at their next sign-in.

Sessions already established are left as they were. Removing a key does not end a session that was signed in with it; ending one is §2.22's.

2.23.6 Access to the socket #

The socket admits the peers §2.20 admits, for the same reason, and §2.20's rules on deciding who may originate a logon and on keeping such peers off the originators' bound on conversations apply unchanged.

2.23.7 An authority that does not offer it #

An authority MAY decline to offer enrolment, or to offer it for some principals. One that declines MUST refuse CredentialEnrollStart with PermissionDenied, and is conforming.

Appendix 2.A Message Reference

Peios / Peios technical reference / PGSS / Logon

2.A.1 Messages #

2.A.1.1 On /run/logon.sock #

msg_typeMessageDirectionDefined in
0x0001LogonStartclient → authority§2.7
0x0002CredentialResponseclient → authority§2.8
0x8001CredentialRequestauthority → client§2.8
0x8002AccessGrantedauthority → client§2.9
0x8003AccessDeniedauthority → client§2.10
0x0020ServiceAttestclient → authority§2.19
0x0030CredentialChangeStartclient → authority§2.20
0x8030CredentialChangedauthority → client§2.20, §2.23
0x0031CredentialEnrollStartclient → authority§2.23
0x0040SessionEndclient → authority§2.22
0x0041SessionEndQueryclient → authority§2.22
0x8040SessionEndedauthority → client§2.22
0x8041SessionEndAllowedauthority → client§2.22

2.A.1.2 On /run/ident.sock #

msg_typeMessageDirectionDefined in
0x0010Lookupclient → authority§2.16
0x0011Enumerateclient → authority§2.17
0x8010LookupReplyauthority → client§2.16
0x8011EnumerateReplyauthority → client§2.17

The high bit marks a message sent by the authority (§2.6). The two ranges are disjoint, and a message of one range MUST be refused on the other socket (§2.14).

2.A.2 Protocol constants #

ConstantValueDefined in
Logon socket path/run/logon.sock§2.5
Identity socket path/run/ident.sock§2.14
MagicPGSL (50 47 53 4c)§2.6
Version1§2.6
Header size12 bytes§2.6
Maximum message size65536 bytes§2.6
Longest grace before a forced end10 seconds§2.22
Longest a SessionEnd may take to answer30 seconds§2.22
Least a client waits for that answer60 seconds§2.21

2.A.3 Field limits #

FieldMaximumDefined in
identifier1024 bytes§2.7
tty128 bytes§2.7
remote_host256 bytes§2.7
supported_credential_types32 entries§2.7
messages8 entries§2.8
prompts16 entries§2.8
answers16 entries§2.8
text512 bytes§2.8
credential_name128 bytes§2.8
data32768 bytes§2.8
home4096 bytes§2.9
shell4096 bytes§2.9
display_name256 bytes§2.9
reason512 bytes§2.10
material16384 bytes§2.23
identity128 bytes§2.19
service256 bytes§2.19
name (lookup key)256 bytes§2.16
sid68 bytes§2.16
qualified_name512 bytes§2.16
withheld32 entries§2.16
values32 entries§2.16
reference name512 bytes§2.16
HOME, SHELL4096 bytes§2.16
DISPLAY_NAME256 bytes§2.16
GROUPS128 entries§2.16
MEMBERS256 entries§2.16
CLAIMS64 entries§2.16
DESCRIPTION1024 bytes§2.16
of_name256 bytes§2.17
cursor, next256 bytes§2.17
entries256 entries§2.17
incomplete32 entries§2.17
incomplete source name32 bytes§2.17

The 68-byte SID maximum is a property of the SID encoding rather than of this chapter: an eight-byte prelude plus the fifteen sub-authorities the one-byte count admits. It is specified in PCDS.

An encoder MUST refuse to produce a field exceeding its maximum; a decoder MUST reject one it receives (§2.6).

Appendix 2.B Enumerations

Peios / Peios technical reference / PGSS / Logon

Adding a value to any enumeration here is a breaking change requiring a version bump — see §2.6, which states the four exceptions and where each is stated in full.

2.B.1 Logon types #

Carried in LogonStart.logon_type (§2.7) as a u8. Semantics are defined by KACS and described in the Peios Kernel TRM; this table is for reference.

ValueName
2Interactive
3Network
4Batch
5Service
8NetworkCleartext
9NewCredentials
10RemoteInteractive

The gaps are deliberate: the numbering follows KACS, and values it does not define are not available here.

A value MAY be added here without a version bump, for the reason §2.7 gives.

2.B.2 Identifier types #

Carried in LogonStart.identifier_type (§2.7) as a u8.

ValueNameidentifier holds
1UsernameA principal name

2.B.3 Credential types #

Carried in Prompt.credential_type (§2.8) and in LogonStart.supported_credential_types (§2.7) as a u8. See §2.11 for when a new one is warranted.

ValueNameCollection
1PasswordA line of text, not echoed
2SshPublicKeyBound SSH candidate or signature proof; §2.D

2.B.4 Message severities #

Carried in Message.severity (§2.8) as a u8.

ValueName
0Info
1Error

2.B.5 Enrolment actions #

Carried in CredentialEnrollStart.action (§2.23) as a u8.

ValueNamematerial holds
1AddThe credential to enrol: for SshPublicKey, one line of an OpenSSH public key file
2RemoveWhich credential to remove: for SshPublicKey, its SHA256: fingerprint

CredentialEnrollStart.credential_type takes a value from the credential types above; only SshPublicKey is defined for enrolment.

2.B.6 Denial codes #

Carried in AccessDenied.denial (§2.10) as a u32.

ValueNameMeaning
1MalformedRequestThe message could not be understood.
2UnsupportedVersionThe protocol version is not implemented.
3PermissionDeniedThe peer may not make this request at all.
4AuthenticationFailedThe principal is unknown, or the credential is wrong. Deliberately one code — see §2.10.
5LogonTypeNotPermittedThe peer may not request this kind of session.
6AccountRestrictedThe principal exists and authenticated, but policy refuses this logon; or, in a credential change, the principal has no credential that can be changed (§2.20).
7AuthorityUnavailableThe authority cannot reach what it needs to decide.
8ConversationLimitToo many rounds, or too long without an answer.
9InternalThe authority failed for a reason it will not describe.
10NoSuchSessionThere is no live logon session by that identifier. Answers only SessionEnd and SessionEndQuery (§2.22).
11CredentialRejectedThe credential offered for enrolment, or named for removal, was refused: unreadable, unsupported, already held, one too many, or not held. Answers only CredentialEnrollStart (§2.23).

A denial code MAY be added without a version bump where it only ever answers messages newer than it is — messages no peer that predates the code can send — because such a peer is then never sent it. NoSuchSession and CredentialRejected were added under this rule. A client of the newer messages SHOULD treat a code it does not recognise as a refusal, reporting the code and reason, rather than as an unreadable answer; it is the one place in this chapter where a client may meet a denial newer than itself.

2.B.7 Key types #

Carried in Lookup.key_type (§2.16) and Enumerate.of_key_type (§2.17) as a u8. Zero in of_key_type means the field is unused.

ValueNameKey is in
1Namename
2Sidsid
3UnixIdunix_id

2.B.8 Object kinds #

Carried in Lookup.kind, LookupReply.kind_found and Enumerate.kind (§2.16, §2.17) as a u8.

ValueName
0Any
1Principal
2Group

Any is not valid in kind_found or in Enumerate.kind.

2.B.9 Fields #

Carried in Lookup.fields, Enumerate.fields and LookupReply.present (§2.16) as a u32 bitmask, and in a withheld entry's field as a single bit.

BitNameValue encoding
0UNIX_IDu32
1PRIMARY_GROUPreference
2HOMEstring
3SHELLstring
4DISPLAY_NAMEstring
5GROUPSarray of references
6MEMBERSarray of references
7CLAIMSarray of claim entries
8ENABLEDu8
9LOGON_TYPESu32
10DESCRIPTIONstring

A bit MAY be added without a version bump, because a reply states which fields it answered and an authority MUST ignore a bit it does not implement (§2.16).

2.B.10 Lookup outcomes #

Carried in LookupReply.outcome and EnumerateReply.outcome (§2.18) as a u8.

ValueName
1Found
2NotFound
3Unavailable
4Refused
5Malformed

2.B.11 Withheld reasons #

Carried in a withheld entry's reason (§2.16) as a u8.

ValueNameMeaning
1AbsentThe field has no value.
2RestrictedThe caller may not have this field.
3DeclinedThe source will not produce it.
4TooLargeIt exists and exceeds one reply; use Enumerate (§2.17).

Appendix 2.C Prior Art

Peios / Peios technical reference / PGSS / Logon

2.C.1 Windows LSA #

The closest predecessor is the Windows Local Security Authority and its LsaLogonUser interface. What is taken and what is deliberately left is worth stating, because the resemblance is close enough that the differences matter.

Taken. The separation of authentication from derivation — establishing who someone is, and then constructing a token for them, are different acts by different rules. The idea that a logon produces both a token and a session, and that the session records which authority vouched for it. The vocabulary of logon types.

Diverged. LSA's authentication packages are DLLs loaded into the LSA process, so a defect in any package is a defect in the most privileged process on the system. Nothing in PGSS Logon admits an in-process extension point; an authority that federates does so across a process boundary of its own choosing.

Rejected. The challenge-response family (NTLM and successors). Challenge-response requires the verifier to store something a response can be recomputed from, which is password-equivalent material — the pass-the-hash failure mode, where stealing the store is as good as knowing the passwords. This chapter requires the opposite property: what is stored MUST NOT be usable to authenticate. See §2.11.

2.C.2 PAM #

Pluggable Authentication Modules supplies the conversation shape: an authority that asks for what it needs, a client that renders prompts without understanding them, and several rounds where policy requires them. That shape is why adding a credential type is a change to authorities and not to every client, and it is adopted wholesale.

What is not adopted is PAM's stacking, in which a credential is offered to each module in turn until one accepts. Trying each source with the password hands every source the credentials of every other source's users, including on typos. Where an authority federates, resolution MUST select the answering party before any credential is collected.

PAM is also an in-process module system, and the objection under Diverged applies to it equally.

2.C.3 POSIX name resolution #

The identity-lookup half of this chapter answers the questions getpwnam, getpwuid, getgrnam, getgrgid and getgrouplist ask, and its field mask, reference encoding and one-round-trip requirement are shaped by what those calls need in one go.

What is not adopted is the passwd record itself. GECOS is not carried (§2.9), the flat name:uid:gid tuple is replaced by a SID plus a set of requested fields, and the reserved-character rules (§2.15) exist precisely because a name that reaches those callers ends up in a colon-and-comma-separated line that nothing else validates.

Nor is the assumption that an absence is authoritative. A world-readable file cannot be unreachable, so POSIX has no vocabulary for "the source that would have known did not answer"; §2.18 exists because a federated authority does, and reporting that as "no such user" memoises an outage as a fact.

2.C.4 Design influences #

Descriptor-passing over ambient authority. The token is transferred as a file descriptor rather than named, so that possession of the conversation is what confers it. There is no window in which a minted token exists under a name another process could reach for.

One conversation per connection. The connection is the conversation's identity, so no correlation identifier exists to be forged or confused. This is deliberately unlike the identity channel of §2.14, and unlike PSI (PSPU §2.7), both of which multiplex because their connections are long-lived.

Capability declaration over negotiation. A client states what it can render and the authority works within it, rather than the two agreeing a version or a profile. That is what lets a credential type be added to authorities without a flag day, and it is why the capability list is the one enumeration whose unknown values are dropped rather than refused (§2.7).

Appendix 2.D SSH Public-Key Authentication

Peios / Peios technical reference / PGSS / Logon

The SSH public-key extension is implemented in authd/lpsd and the Peios OpenSSH port. PEI-67 tracks its release qualification. PGSS/PSI version 1 and the unpublished lpsd store layout were replaced together without a version bump or migration. Rebuild consumers together and explicitly reprovision old development stores; lpsd never resets an unreadable store automatically.

An SSH public key is a credential of a principal. The principal source stores the authorized key and verifies proof of possession. The logon authority constrains the originator, applies policy, derives the token, and grants the logon. SSH does not introduce a second account authority.

The first source implementation is lpsd. No .ssh/authorized_keys lookup is performed. Client private keys and server host keys are not principal credential records; their storage is outside this extension.

2.D.1 Responsibilities and trust #

ComponentResponsibility
SSH clientProduces the standard SSH authentication signature using its private key
SSH transport and monitorEstablishes and retains the live transport binding; mediates credential collection
authdVerifies the local originator, relays the credential exchange, enforces logon policy, and grants a token
lpsdOwns local public-key credentials, checks their eligibility, verifies signatures, and asserts the principal

A network-facing process does not receive authority to assert a principal or obtain arbitrary logon tokens. The protected SSH monitor owns the PGSS connection. A user session receives its resulting token, not the PGSS connection, proof buffers, or monitor control channel.

Public-key verification does not replace disabled-account checks, logon type restrictions, originator restrictions, or normal token derivation. A valid signature alone never creates a session.

2.D.2 Principal storage and policy #

lpsd extends each principal with an explicit authentication policy and a bounded list of SSH public-key records. The existing principal SID remains the account identity; names, key comments, and fingerprints do not replace it.

Each key record contains:

FieldRepresentation
Record IDRandom 128-bit identifier, stable until deletion
Public keyValidated, canonical SSH public-key blob; at most 8192 bytes
LabelUTF-8 text, at most 128 bytes; no control characters
Created timeUTC Unix seconds, recorded by lpsd

The fingerprint is derived as SHA-256 of the canonical SSH public-key blob and displayed in OpenSSH's SHA256: base64 form without padding. It is not separately stored as an authority. Labels are display metadata, not executable authorized_keys options. There are at most 32 keys per principal; duplicate key material within a principal is rejected. The same key may be deliberately enrolled on different principals, but a proof for one username does not authenticate another.

The initial algorithm set is plain Ed25519 and RSA with rsa-sha2-256 or rsa-sha2-512 signatures. RSA moduli are 3072–8192 bits. RSA key blobs use the SSH ssh-rsa key format; that does not enable SHA-1 signatures. DSA, SHA-1 signatures, certificates, security-key variants, and additional key families are rejected until their semantics are specified. Malformed keys, noncanonical encodings, trailing data, and unsupported types are rejected at enrollment and at authentication. A maintained SSH key/crypto implementation performs decoding and verification; no new cryptographic primitive is introduced here.

Authentication policy has an explicit mode:

  • Credentials: a nonempty set of permitted methods, initially Password and/or SshPublicKey. One permitted method is sufficient.
  • NoCredential: authentication is intentionally unnecessary.
  • Denied: no new authentication is permitted by this source.

The principal's separate enabled flag is checked first. In Credentials mode, a permitted method without corresponding usable credential material fails; it does not select another mode. In particular, deleting the last key on a key-only principal leaves that principal unable to authenticate. NoCredential mode is never inferred from an empty verifier or key list.

Keys are enrolled in two ways. An administrator enrolls them through lpsd's protected administrative interface: the lps key add, lps key list, and lps key remove operations operate on one principal and use its existing administration authorization checks. A principal enrolls and removes their own through PGSS Logon's CredentialEnrollStart (§2.23), described under Self-service enrollment below. Neither way silently changes the principal's authentication policy. An explicit policy operation selects password-only, key-only, either credential, NoCredential, or Denied. NoCredential and Denied cannot be combined with credential methods.

Mutations are atomically persisted before success is acknowledged, using the store's existing rollback-on-write-failure behavior. They invalidate affected authentication state. Enrollment accepts a plain OpenSSH public key line, including an optional label, but rejects private-key input and authorized_keys options rather than silently ignoring them. Credential inventory is not exposed through unauthenticated identity lookup.

Compound authentication such as password AND key is a separate extension.

2.D.3 Self-service enrollment #

A principal adds or removes their own SSH public keys with CredentialEnrollStart on /run/logon.sock (§2.23). The account is the user of the caller's token; the message names nobody. Possessing a session token does not by itself authorize adding a persistent credential, so both adding and removing re-prove the principal's current password inside the conversation, and a wrong one ends it with AuthenticationFailed and the fixed Authentication failed. wording.

authd routes the conversation to the source that holds the principal as PSI's EnrollCredential (PSPU §2.23), and only to a source declaring the ENROLLS_CREDENTIALS capability; a source that does not is sent nothing, and the caller is refused PermissionDenied. lpsd declares it, and:

  • resolves the principal from the SID authd vouched for, with the rules a password change uses. A principal with no password to prove — policy key or none, or a password policy with no password set — is refused AccountRestricted, with words telling them to ask an administrator, before anything is asked;
  • checks an added key line, or a removal's fingerprint, by the same store paths lps key add and lps key remove use — the same import, algorithm and size rules, duplicate rejection, 32-key limit and last-administrator rule — before asking for the password, and again when it applies the change. Material it refuses is CredentialRejected, with the store's reason;
  • names a key for removal by its SHA256: fingerprint rather than the record ID lps key remove takes, because the fingerprint is what a person can read from their own .pub file. The key's comment becomes its label;
  • persists the change with the administrative write's rollback, sends authd the same change notification an administrative write sends, and only then answers CredentialChanged.

Adding a key does not let it sign the principal in unless their policy is key or either, which only an administrator sets. A principal whose policy is key cannot use this path at all, since they have no password to prove themselves with; removing the last key of such an account is therefore never reachable here.

2.D.4 Store replacement and revocation #

The unpublished store layout is replaced in place, retaining its existing format-version marker. The reader and writer implement only the replacement layout; there is no old-store importer, dual reader, or downgrade path. Development images and test stores are explicitly reprovisioned from updated inputs rather than carried forward from an earlier build.

Provisioning writes an explicit policy for every principal. Password-bearing fixtures use Credentials/Password; intentionally passwordless fixtures, including the live image account, explicitly use NoCredential. Key-only fixtures use Credentials/SshPublicKey with enrolled keys. An empty verifier or key list never selects NoCredential automatically.

Unreadable, truncated, or malformed state still fails closed. Clean replacement does not mean that lpsd silently deletes a store or creates new identities on a decode failure. Reprovisioning is an explicit development/build operation.

lpsd keeps a per-principal credential generation in pending conversations. Immediately before issuing an assertion it rechecks the principal is enabled, the method remains permitted, the selected key is still enrolled, and the generation has not changed. A changed generation terminates the attempt; the client may start again. A positive candidate-key probe is never cached as authorization for a later signature.

The assertion is the authentication decision's ordering point: removals committed before it take effect for that attempt; removals committed after it affect subsequent attempts. Already issued tokens and sessions are not terminated by deleting a key. Revoking an existing session remains an explicit session operation.

2.D.5 Coordinated protocol replacement #

SshPublicKey is credential type 2; Password remains type 1. PGSS Logon and PSI retain their existing header version 1, magic, framing conventions, and message-size ceilings. Each implements one codec for the revised layout. There is no version negotiation, translation between old and new codecs, or automatic fallback to the previous layout or a weaker credential.

This deliberately replaces unpublished contracts. The implementation updates the existing enum-extension/version wording and message definitions together with the code. authd, lpsd, login, GXWI, peinit, utilities, other active consumers, and their dependency pins are rebuilt as needed for the revised shared types. Old development binaries and stores are not a supported combination with the replacement. Existing header-version and malformed-message checks remain; they are not a compatibility mechanism for two layouts sharing version 1.

LogonStart contains required_credential_type, a u8: 0 leaves the source's ordinary policy in control, 1 requires Password, and 2 requires SshPublicKey. A nonzero value must appear in supported_credential_types. PSI Authenticate carries the same requirement; its Assertion reports authenticated_credential_type (0 means no credential). authd rejects a result that fails a nonzero requirement. The SSH monitor selects 1 or 2 for its current method, so even an intentionally NoCredential principal cannot produce an unearned successful SSH password/key authentication. Rebuilt console and graphical clients retain their intended behavior by explicitly declaring their capabilities and credential requirement.

The new fields follow the existing fields within their enclosing length-framed structure. LogonStart appends required_credential_type, then has_ssh_binding (u8, exactly 0 or 1), then the binding structure when present. PSI Authenticate carries this extended LogonStart. Assertion appends authenticated_credential_type. Prompt appends parameters. These tails are mandatory even when their values are zero or empty; truncation is an error, not permission to infer permissive defaults. Binding fields are encoded in the order listed below: 16 raw connection-ID bytes, a byte string session identifier, and a UTF-8 username. A binding is required for type 2 and forbidden on attempts that do not advertise that type. Unknown method values, invalid presence flags, and type-2 assertions in a Password-only exchange are rejected.

PGSS and PSI share the revised credential bodies. The authority validates and relays them without a legacy translation layer. The SSH frontend advertises only the method of the current SSH authentication attempt, so a separate password attempt remains possible only when policy allows it.

A client advertising only Password is never sent type 2. A key-only account reached through such a client is denied, not prompted for an invented password and not granted without credentials. Identity, service-attestation, and credential-change operations retain their intended behavior in the coordinated build. This extension does not enable SSH key changes through the password-change operation.

The new credential type is offered only to a client that declares it. The authority also checks that the originator is allowed to provide SSH transport bindings. Ordinary password collectors do not acquire that authority by adding type 2 to their capability list.

2.D.6 Binding the proof to a connection #

Standard SSH signs the initial transport session identifier together with the authentication request. It does not sign a fresh arbitrary challenge invented by PGSS. The client-facing exchange remains RFC 4252 public-key authentication; no Peios client extension is required.

For a key attempt, LogonStart includes a typed SshTransportBinding structure. Its fields are a monitor-generated 16-byte connection ID, the initial SSH session identifier (16–64 bytes), and the exact SSH username (1–256 UTF-8 bytes). The username equals LogonStart's Username identifier byte for byte. Other identifier types are rejected for this credential. The SSH service is fixed to ssh-connection and the method to publickey for this first implementation. The binding is copied into PSI Authenticate from authd's validated conversation state, not from an Answer.

The monitor's connection ID is a correlation value, not proof or a bearer capability. authd accepts binding claims only from a specifically authorized service originator verified through its kernel socket-peer token. The implementation requires an unrestricted SYSTEM peer with the enabled, non-deny-only service SID derived from the fixed service name sshd, in addition to the ordinary originator/logon-type checks. This is an SSH-specific gate, not a general service-group policy union. The network child and logged-on user must lack that service authority and access to its PGSS descriptors.

The monitor obtains the initial session identifier from trusted key-exchange state and pins it for the connection, including across rekeys. It does not accept an arbitrary hash supplied through its authentication IPC. The trusted monitor creates a fresh 16-byte server KEXINIT cookie. Before signing the initial exchange hash it validates the bounded structured transcript, requires that cookie and its own host public key, and recomputes the SHA-256 or SHA-512 exchange hash. It supports ML-KEM/X25519, sntrup761/X25519 and Curve25519 exchanges. Simply remembering the first hash a child asks the host key to sign is not accepted as proof of freshness against a compromised child.

This is an explicit trust boundary: lpsd verifies the signature, while the SSH transport component vouches for the live connection. lpsd cannot independently prove network freshness from a caller-supplied transcript. A compromised authorized transport component is outside the guarantee; an ordinary local process or compromised network parser is not.

All candidate offers on one SSH connection retain the same binding. The monitor serializes attempts, fixes the target username for that connection, and rejects username changes. Disconnect, monitor exit, loss of authd, or successful authentication makes pending attempts unusable. A connection receives at most one granted logon token. The monitor does not let a child open an independent PGSS conversation using a previously accepted proof.

2.D.7 Credential exchange #

Prompt adds a length-framed parameters byte string, at most 4096 bytes. It is empty for Password. For SshPublicKey it contains a length-framed structure containing a disposition byte: 0 initially, 1 for the previous candidate being acceptable, and 2 for the previous candidate being rejected. It describes only that candidate in that conversation; it never asserts identity or grants access. There is one outstanding prompt, with a fresh credential_ref for each round.

An SshPublicKey Answer.data contains a length-framed structure:

FieldEncoding and limit
Operationu8: 0 Candidate, 1 Proof
Signature algorithmUTF-8 string, 1–64 bytes
Public keyByte string, 1–8192 bytes
SignatureByte string, 0–8192 bytes; empty exactly for Candidate

The outer structure uses PGSS little-endian lengths. The public-key blob and signature blob retain SSH's own encoding, including its big-endian lengths. Unknown operations fail closed. Total answer data remains below the existing 32768-byte ceiling; the parser additionally rejects trailing fields outside this layout. No signed-data blob or replacement session identifier is accepted in an answer.

For Candidate, lpsd checks whether that key and algorithm would be eligible for the named principal and returns another SshPublicKey prompt with the corresponding disposition. The monitor maps this to SSH's key-acceptable reply or authentication failure. Unknown principals, disabled principals, disallowed methods and unregistered keys all produce the same negative result. Only one submission is in flight; the monitor consumes a disposition before sending the next answer, abandons the PGSS exchange if the SSH client switches methods, and never reports a probe response as logon success. The positive result discloses eligibility of an already-presented key, as standard SSH probing does; it does not enumerate keys.

For Proof, lpsd repeats eligibility checks and verifies the signature. A client may send Proof without a preceding Candidate. Candidate success never eliminates verification or the final account-state check. A bad proof ends the PGSS attempt with a generic authentication failure. Trying another key uses the next bounded SSH attempt; no proof is silently reinterpreted as a different credential.

lpsd reconstructs the signed bytes exactly as RFC 4252 specifies: the validated initial session identifier, message number 50, exact username, ssh-connection, publickey, boolean true, algorithm and key blob. It checks the algorithm agrees with the key and the signature's internal algorithm identifier. There is no “verify these arbitrary bytes” API. Case-insensitive principal lookup may resolve the username, but the signed username is never case-normalized before signature verification.

When proof succeeds, lpsd emits the ordinary PSI assertion. The source does not mint a token or choose additional privileges based on the key. authd's AccessGranted is the only result permitting SSH authentication success and session startup. Password and key are alternatives in the initial policy model; partial success for multifactor authentication is not implemented or advertised.

2.D.8 Bounds, logging and failure behavior #

One SSH connection has at most 16 public-key submissions total, counting Candidate and Proof, and at most 6 failed signed/password attempts. Probes consume the submission budget but do not by themselves trigger persistent account lockout. The whole authentication exchange has a 120-second deadline, without resets for probes or switching methods. Existing tighter system limits still apply. Unknown users take the same bounded failure path; no guarantee of exact constant-time public-key lookup is made.

The authority logs the originator, logon type and result; the source logs the selected key fingerprint on success, and granted sessions carry their principal/session identifiers. Neither passwords, complete proofs nor private material appear in logs. PGSS wire credentials use the existing self-wiping buffers. Parsed public keys and SSH signatures are short-lived public cryptographic data; neither is a password-equivalent secret or reusable bearer token.

Cryptographic verification failure, unsupported algorithm, revoked key, disabled principal and disallowed method produce generic authentication failure to the remote client. Detailed operational reasons are available only through privileged local diagnostics. Invalid framing and state transitions terminate the relevant conversation; they never become a passwordless attempt.

2.D.9 Qualification requirements #

Required regression coverage includes:

  • Successful key-only and password-only logons, and either-method policy; unsupported method and credential-free downgrade refusal.
  • Rebuilt console/GXWI password logons and explicitly provisioned passwordless accounts, including fresh provisioning, restart, and failed store writes.
  • Candidate acceptance without any assertion, direct Proof, multiple-key clients, budget exhaustion, cancellation, and identity-change refusal.
  • Wrong signature, mismatched algorithm, wrong username/service/key, malformed nested lengths, unsupported versions, and cross-protocol input.
  • Key removal, policy change and account disablement between Candidate and Proof and immediately before assertion; deletion of the last key.
  • Captured proof replay on another SSH connection, an ordinary local PGSS connection, a new monitor, and after authd restart. Include a hostile network child proposing an old key-exchange hash, not just an honest client connecting twice.
  • No PGSS or monitor descriptors, originator service authority, or elevated launch privileges inherited by the principal's shell.
  • The final shell's actual primary token carries the intended principal, logon session, groups and policy, verified against kernel state.

2.D.10 Service operation #

The package dev.peios.openssh provides the Peios OpenSSH server and ordinary SSH command-line tools. Its source repository owns the port and Pekit recipe; there are no downstream source patches in the package catalogue.

All SSH logons use RemoteInteractive, including commands without a PTY and SFTP. Password authentication requires an actual password; an intentionally NoCredential account cannot log in through SSH. TCP, Unix-socket, agent, X11 and tunnel forwarding are disabled in this first port.

The sshd-service.reg and sshd-network.reg vendor seeds are separate image opt-ins. The Experimental edition enables both. make boot in dist/release forwards localhost TCP 2222 to the guest's TCP 22. The service runs as SYSTEM with only SeChangeNotifyPrivilege, SeAssignPrimaryTokenPrivilege and SeTcbPrivilege. The network seed permits inbound TCP 22 and reserves that port for the sshd service SID. The network child loses its privileges and service-group authority, receives an Everyone restricting SID, and retains upstream chroot and seccomp protection. The session child installs the granted principal token before accepting the post-authentication network state.

Defaults are /usr/etc/ssh/sshd_config; administrator overrides may use /lcl/etc/ssh/sshd_config. The port and password authentication are not chosen there. They are machine policy, Machine\Software\OpenSSH Port and PasswordAuthentication (both REG_DWORD, seeded 22 and 1), which start-sshd reads at each start and passes to sshd as -p and -o PasswordAuthentication=; -p makes sshd ignore every Port line. The port, the service SID's reservation of it and the ssh firewall rule's DstPort.Equal must agree, so the package's ssh-settings moves all three in one registry transaction and then restarts the service; see Signing in over SSH. The service generates an Ed25519 host key on first start in protected persistent /var/state/sshd, validates and reuses it on subsequent starts, and fails on an invalid existing key. It never rotates a key automatically. Client private keys remain with the client.

Provision a key-only account while disabled to avoid a temporary NoCredential window:

lps add alice --no-password --disabled
lps key add alice /path/to/alice.pub laptop
lps policy alice key
lps enable alice
lps key list alice

Use lps policy alice either to allow either an enrolled key or an existing password. Administrative key removal uses the record ID printed by lps key list: lps key remove alice RECORD_ID. Self-service removal names the key by fingerprint instead (§2.23). Changing material does not change policy.

2.D.11 References #

  • RFC 4252, section 7: candidate-key queries, direct signed requests, and exact signed fields.
  • RFC 8332: RSA SHA-2 signature algorithm names and reuse of the RSA public-key encoding.
  • OpenSSH Portable monitor: upstream privilege-separation integration reference; it does not by itself establish Peios originator authorization or this contract's trusted-transport freshness requirement.

3.1 Scope and Roles

Peios / Peios technical reference / PGSS / MSIP

This chapter specifies PGSS MSIP, the Multi-Surface Interaction Protocol: the protocol by which a daemon or long-running operation drives one or more generic user-interface surfaces — showing information, reporting progress, and requesting input — without knowing or caring how any of them renders.

Two roles participate.

The daemon owns the work and the decisions. It offers one or more conversation kinds on a socket, issues the conversation's pages, decides what every answer means, and decides what page follows. Only the daemon knows the shape of the conversation.

The surface renders. It draws the elements it is sent, collects the person's input, and returns answers without interpreting them. A TUI, a graphical installer, a web page and an automated agent are all surfaces, and the daemon cannot tell them apart except by what they declare they can render.

The person at the surface is not a party to the conversation, in the same sense as the principal of a logon (§2.1).

Both roles are publicly implementable. A third party MAY ship a new surface for an existing daemon, a new daemon driven by existing surfaces, or both, and interoperate with the other half unchanged. That substitution is the point of the protocol: a daemon that offers a conversational interface to generic surfaces MUST offer it as specified here, precisely so that surfaces need not be written per daemon.

This chapter covers:

  • the shape of a conversation: who opens it, who drives it, how many surfaces may attach, and how concurrent answers are arbitrated (§3.3)
  • what each role must establish for itself rather than believe from a message, and the access control governing conversations (§3.4)
  • message framing and the rules under which the format may be extended (§3.5)
  • how a connection binds to a conversation (§3.6)
  • turns, elements, answers, and mid-turn state (§3.7 to §3.10)
  • how a conversation ends, and what a protocol error is (§3.11)
  • the obligations binding on each role (§3.12)

This chapter does not cover:

  • What any conversation is about. The pages of a particular daemon — an installer's disk selection, a first-boot flow's account page — are that daemon's specification, not this chapter's.
  • How a surface renders any element. Rendering is deliberately unspecified; §3.8 states the little a surface owes the daemon.
  • Unattended operation. A machine-driven answering agent is a surface like any other; the policy by which it answers is outside MSIP.

3.2 Terminology

Peios / Peios technical reference / PGSS / MSIP

Conversation. One instance of a daemon driving one flow: a single installation, one first-boot setup, one authorisation request. A conversation belongs to the daemon, outlives any particular connection, and is identified by an unguessable identifier the daemon assigns.

Kind. The name of a flow a daemon offers — install, oobe. A daemon lists its kinds at greeting time; a surface opens a conversation by kind.

Turn. One page of the conversation. A turn is an ordered list of elements issued as a unit, identified by its seq — a number the daemon increments for every turn of a conversation. The seq is the only identity a surface cites. A turn MAY also carry an id, a stable template identity for surfaces that want to special-case a known page: two turns with the same id MUST present the same set of element refs and types, though their state may differ. An id is not unique within a conversation and is never cited in a message.

Element. One item on a page: a piece of text, an input, a progress report, an action. An element has a type from the negotiated set (§3.6) and a ref.

Ref. The semantic identity of an element: what is being asked, not where it appears. A ref is unique within its turn, stable across turns and across runs of the same daemon, and documented by the daemon's own specification. A ref recurs freely — a question re-asked after a validation failure, or revisited because the person went back, keeps its ref. Nothing about the sequence of turns is stable; only refs and their answer types are.

Action. An element a surface presents as something to press rather than something to fill in. Actions carry no value; an answer names at most one, and the daemon alone knows what it means (§3.9).

Attach. Joining a connection to an existing conversation. Any number of connections may be attached to one conversation at once; every turn and update is broadcast to all of them.

Class. A presentation hint, on turns and on elements: a list of tokens a surface MAY use to style what it renders. Classes promise nothing and are never cited in a message.

3.3 The Conversation

Peios / Peios technical reference / PGSS / MSIP

3.3.1 A surface opens; the daemon drives #

Every conversation is opened by a surface (§3.6). Within it, the daemon speaks first and always: it issues every turn, and a surface never sends anything but an answer to the turn that is outstanding.

The invariant is about who opens, not who has the need. A daemon that wants to put a question to whoever is around — a passphrase prompt at boot, an authorisation request — is served by a surface that opened an agent-style conversation earlier and sat waiting in it. The daemon still drives; the surface still merely opened.

3.3.2 Broadcast, and the first valid answer #

A conversation is not a session with one surface. Every attached connection receives every TURN and every UPDATE, and any of them may answer the outstanding turn.

Arbitration is by seq:

  • The first valid answer to the outstanding turn is applied. The daemon then either issues the next turn — which is the only acknowledgement of acceptance — or ends the conversation.
  • An answer that fails validation does not close the turn. The daemon broadcasts the failure as element errors (§3.10) and the turn remains open to every surface, including the one that failed.
  • An answer to any other seq is stale, and MUST be ignored silently. No reply is owed: the broadcast that made it stale is already on the wire to its sender, and reaching the loser is the transport's job, not arbitration's.

Two people on two surfaces can race each other's intent. That is accepted: access control (§3.4) already admitted both, and the situation is no different from two administrators sharing a shell.

3.3.3 Persistence #

A conversation outlives its connections. A surface that crashes, a terminal that disconnects, a web page that is closed — none of these end the conversation; the daemon carries on, and a new connection may attach and continue where the person left off.

A surface that attaches mid-conversation receives the outstanding turn in its current state — the turn as issued, with every update since folded in (§3.10). It does not receive history. There is deliberately no transcript-replay mechanism in this protocol.

3.3.4 Lifecycle #

A conversation exists from the moment the daemon creates it (§3.6) until the daemon broadcasts END (§3.11), after which its identifier is meaningless and MUST NOT appear in a listing.

A daemon SHOULD bound the life of an abandoned conversation — one with no attached surfaces and no progress — by policy of its own: expiry after an interval, a cancel decision, or indefinite patience are all legitimate. This chapter requires only that whatever happens is reported as an END to anyone attached when it happens.

How many conversations of a kind may exist at once is daemon policy, not protocol. A daemon enforcing a singleton — one ongoing installation, say — refuses nothing: it answers a START for a kind that is already running by attaching the caller to the existing conversation, and says so (§3.6).

3.4 Trust and Access Control

Peios / Peios technical reference / PGSS / MSIP

3.4.1 The asymmetry #

MSIP exists to separate privilege from presentation. The daemon typically holds the authority the work requires; the surface renders, and holds none. The protocol is designed so that nothing a surface sends can be more than a claim, and nothing a daemon sends obliges a surface to do more than draw.

3.4.2 What the daemon must not trust #

Everything a surface sends is input from an untrusted peer. Specifically, a daemon:

  • MUST NOT treat any answer as authorisation. Whether the peer may do what the answer asks is established by the access decision of §3.4, made from the connection's verified peer identity — never from the content of a message.
  • MUST NOT vary its behaviour on Hello.surface (§3.6). It is an observability string.
  • MUST validate every answer against the outstanding turn (§3.9) rather than assume a well-behaved surface.

3.4.3 What the surface must not trust #

A surface renders what it is sent, but:

  • MUST NOT render a secret element's value back to the person beyond the masked input it collects (§3.8), whatever the daemon sends.
  • MUST NOT interpret informational text, and MUST NOT vary its behaviour on the content of names, help text, messages, or classes. Text is for the person; behaviour follows structure only.
  • MUST fail a page it cannot render rather than guess. The negotiation of §3.6 makes this unreachable in a conforming pair; the rule is what a surface does when it is reached anyway.

3.4.4 Who may connect, and who may attach #

Access to the socket is governed by a security descriptor on the socket path, like any object. Offering separate conversation kinds to separately-trusted callers on one socket is the daemon's problem; where the difference matters, the daemon MUST check the connection's verified peer identity per kind at START, and per conversation at ATTACH and LIST.

A conversation's identifier is unguessable (§3.6), but secrecy of the identifier is not the control: the access check at ATTACH is. A LISTING MUST contain only conversations the caller could attach to.

3.4.5 Secrets #

An element with secret set collects material that must not escape:

  • A surface MUST collect it without echo, MUST NOT log it, and MUST NOT retain it beyond the answer that carries it.
  • A daemon MUST NOT place a secret value in any element state, in any UPDATE, or in any error text — the answer is the only message that ever carries it. A late joiner therefore never receives another surface's secret, because the outstanding turn it is sent contains only the ask.
  • Neither role logs a secret value under any verbosity.

3.5 Message Framing

Peios / Peios technical reference / PGSS / MSIP

3.5.1 Header #

MSIP reuses the header layout of §2.6 with its own magic. Every message begins with a 12-byte header:

OffsetSizeFieldValue
04magicMSIP (4d 53 49 50)
42version1
62msg_typeSee §3.A
84total_lenHeader plus body, in bytes

The rules of §2.6 apply unchanged to the magic (checked on every message; wrong magic is an immediate whole-connection failure), to the version (an unimplemented version MUST be refused; here, by closing the connection, optionally after an Error where one can still be encoded), and to total_len (self-delimiting; a decoder MUST reject a declaration exceeding the limit without reading the body, and one smaller than the header).

The high bit of msg_type marks a message sent by the daemon; surface messages have it clear. As in §2.6, this is a readability property — a peer MUST validate the type it received against what it expected, not merely against the direction bit.

A message MUST NOT exceed 1 MiB in total. The limit is larger than the logon channel's because log elements legitimately carry bulk; it is a limit all the same.

3.5.2 Body #

The body is one JSON object, UTF-8 encoded, as defined by RFC 8259. Which object each message type carries is specified in §3.6 to §3.11 and tabulated in §3.A. The message type lives in the header only; the body does not repeat it.

A decoder MUST reject a body that is not valid UTF-8, not valid JSON, or not an object, as a protocol error (§3.11).

3.5.3 Extensibility #

The JSON body extends by different rules than the binary codec, and they are binding on anyone revising this chapter:

  • A decoder MUST ignore an object key it does not know. New fields are added as new keys, and MUST be optional with a safe default, because older peers will not send them and will not read them.
  • An absent optional field and a field carrying its documented default MUST NOT be distinguished.
  • A peer MUST treat an unknown value of an enumerated field — Refused.reason, Error.code, End.outcome — as a protocol error. Adding a value to an enumeration is a breaking change and requires a version bump.

Element types and element state are the deliberate exception, as supported_credential_types is to Logon (§2.6): the set a surface renders is a statement of capability negotiated at HELLO, a daemon MUST NOT send an element type the surface did not declare, and a type's state keys are defined by the element type (§3.B), not by the message format. Adding an element type is therefore not a version bump — an old surface simply does not declare it.

3.6 Binding

Peios / Peios technical reference / PGSS / MSIP

3.6.1 One conversation per connection #

A connection carries at most one conversation, for its whole life. There is no multiplexing: a surface that wants two conversations opens two connections, and closing the connection is how a surface detaches. What a connection may send depends only on which of three states it is in: expecting-hello, unbound, or bound.

3.6.2 Greeting #

The first message on a connection MUST be the surface's Hello:

FieldMeaning
element_typesThe element types this surface can render.
surfaceOptional observability string ("install-tui/0.1").

element_types is a statement of capability. The daemon MUST NOT send this connection an element whose type is absent from it, and MUST refuse a binding whose conversation needs more than the surface declared (unsupported_elements) — refusing at bind time is what makes an unrenderable page unreachable, rather than a failure mid-flow. surface is for logs; the daemon MUST NOT vary behaviour on it.

The daemon replies Welcome, carrying the conversation kinds it offers and its own optional observability string.

3.6.3 The unbound connection #

After WELCOME the connection is unbound. It may send exactly three things:

  • List — the daemon replies Listing: the conversations this caller could attach to, each with its identifier, kind, and current seq. A daemon with nothing listable replies with an empty listing.
  • Start — open a conversation of a kind.
  • Attach — join the conversation named by identifier.

Anything else on an unbound connection is a protocol error, as is a second binding attempt on a bound one.

3.6.4 Binding #

A successful Start or Attach binds the connection and is answered by Bound, carrying the conversation's identifier, its current seq, and attached — true when the caller joined a conversation that already existed. Immediately after a Bound, if a turn is outstanding, the daemon MUST send it to the new connection in its current state (§3.3).

A daemon MAY answer a Start by attaching: a daemon whose policy admits one conversation of a kind answers a second Start with Bound { attached: true } for the existing one. The surface learns what happened from attached and nothing else changes.

A conversation's identifier is assigned by the daemon at creation: 32 lowercase hexadecimal characters encoding 128 bits that MUST come from a cryptographically secure source. It names the conversation in Attach, Bound and Listing, and nowhere else.

A binding that is refused is answered by Refused and leaves the connection unbound:

reasonMeaning
access_deniedThe access check of §3.4 failed.
unknown_kindStart named a kind this daemon does not offer, or Attach named a conversation that does not exist or has ended.
unsupported_elementsThe conversation needs element types the surface did not declare.
unavailableThe daemon offers this kind but could not open a conversation.

A daemon SHOULD prefer unknown_kind over access_denied for a conversation the caller may not know exists, and MUST NOT use Refused for anything but a binding attempt.

unavailable is the daemon's own failure and nothing to do with the surface: the kind exists, the caller may have it, and the surface can render it. A daemon SHOULD say what went wrong in message. Without this reason such a daemon can only hang up, and a surface cannot tell a broken daemon from a slow one — it waits on a turn that will never come, showing nothing.

3.7 Turns

Peios / Peios technical reference / PGSS / MSIP

3.7.1 Issuing #

The daemon advances the conversation by broadcasting Turn:

FieldMeaning
seqThis turn's sequence number.
idOptional template identity (§3.2).
nameOptional display title.
classOptional presentation hints.
elementsThe page, in order.

seq starts at 1 and increments by exactly 1 per turn of a conversation. Issuing a turn implicitly closes the previous one: answers to it become stale (§3.3). A daemon MUST NOT reuse or rewind a seq within a conversation.

The elements are an ordered list, and the order is the daemon's one presentational statement: it MUST order elements helpfully, and a surface SHOULD present them in that order — but MAY depart from it, and a daemon MUST NOT depend on the order being visible. Everything else about layout belongs to the surface. This protocol deliberately has no vocabulary for columns, grouping, emphasis or geometry, and anyone extending it SHOULD refuse the temptation: an element says what is being asked, never how it looks.

3.7.2 Turns that expect no answer #

A turn with no enabled input elements and no enabled actions expects no answer: the daemon advances past it on its own — typically a progress page, updated in place (§3.10) until the work completes and the next turn or the END follows. A surface MUST NOT be required to acknowledge such a turn, and the daemon owes it nothing before moving on.

A turn that does expect an answer stays outstanding until a valid answer is applied (§3.9) or the daemon supersedes it — the daemon remains free to issue a new turn at any time, because the flow is its own.

3.8 Elements

Peios / Peios technical reference / PGSS / MSIP

3.8.1 Common fields #

Every element, of every type, is a JSON object with these fields:

FieldDefaultMeaning
ref—Semantic identity (§3.2). Unique within the turn.
type—An element type from the negotiated set.
nameabsentDisplay label.
class[]Presentation hints.
helpabsentLonger explanatory text.
requiredfalseAn answer that validates must supply a value.
defaultabsentValue assumed when the answer omits this ref.
secretfalse§3.4's handling rules apply.
enabledtrueDisabled elements render but cannot be answered.
errorabsentValidation failure text, set by UPDATE (§3.10).

All further keys are state, defined per element type (§3.B).

name and help are for the person. A surface MAY substitute its own text — translations keyed on the element's ref or the turn's id — and the daemon-supplied text is the fallback; this is the protocol's entire localisation mechanism, and it is why refs and template ids are stable.

3.8.2 Disabled elements #

An element with enabled: false is present but inert: a surface SHOULD render it as visibly unavailable (with its help explaining why), MUST NOT collect input for it, and an answer carrying a value for it is a protocol error. Disabled elements exist so a flow can show what it will someday ask — a capability the daemon does not yet have loses none of its place on the page by being unimplemented.

3.8.3 Inputs, outputs, and actions #

Element types divide three ways, and the division is what a surface may rely on:

  • Output types (text, progress, log) inform. They are never answered, are exempt from required, and exist to be updated in place.
  • Input types (string, boolean, select) collect a typed value, returned in the answer under the element's ref.
  • action is pressed, not filled. Actions carry no value and no unattended meaning beyond flow: an automated surface that can answer every input on a page from its own sources presses the page's primary action and needs to understand nothing else.

3.9 Answers

Peios / Peios technical reference / PGSS / MSIP

3.9.1 Shape #

A surface answers the outstanding turn with Answer:

FieldMeaning
seqThe turn being answered.
actionThe action pressed, by ref. At most one.
valuesInput values, keyed by ref.

values MUST name only enabled input elements of the turn named by seq; a value for an unknown ref, a disabled element, an output element or an action is a protocol error. A value's JSON type must match the element type's answer encoding (§3.B).

3.9.2 Actions #

If the turn has any enabled action, the answer MUST name exactly one of them; if it has none, the answer MUST omit action — the implicit submit. Naming anything that is not an enabled action of the turn is a protocol error.

What an action means — next, back, cancel, begin — is the daemon's business alone. A surface presents actions and reports which was pressed; it MUST NOT ascribe navigation semantics of its own.

3.9.3 Validation #

An answer validates unless the action it names carries validate: false (§3.B) — the escape that lets Back and Cancel leave a half-filled page. When an answer validates:

  • every enabled, required input element MUST have a value in values;
  • an omitted optional input with a default is treated as answering with that default.

The daemon then applies its own domain validation — is that disk still present, is that name well-formed — which this chapter does not constrain.

3.9.4 The three fates #

An answer is exactly one of:

  • Stale — seq is not the outstanding turn. Ignored silently (§3.3).
  • Applied — structurally sound and valid. The acknowledgement is whatever the daemon broadcasts next: the following turn, or END. The winning answer itself is never echoed to anyone.
  • Failed validation — structurally sound, but required values are missing or domain validation failed. The daemon broadcasts an UPDATE setting error on the offending elements; the turn stays open for every surface (§3.3).

A structurally unsound answer — the protocol errors above — is none of these: it is answered by Error on the offending connection only (§3.11), and the turn is unaffected.

3.10 Updates

Peios / Peios technical reference / PGSS / MSIP

The daemon changes the outstanding page without a new turn by broadcasting Update:

FieldMeaning
seqThe outstanding turn.
fullReplacement or patch.
elementsSee below.

A sparse update (full absent or false) carries patches: objects naming an existing element by ref plus the fields that change — a progress value, appended log lines, an error, an enabled flip, a refreshed choices. A patch key set to null removes that field. A patch MUST NOT name a ref absent from the turn, and MUST NOT carry a secret element's collected value (§3.4).

A full update (full: true) carries complete elements and replaces the turn's element list. It is the only way the set of refs may change mid-turn. Changing the set is legal but SHOULD be rare — a page that reshapes itself is usually two turns — and a surface's obligation is only to re-render from scratch, which is why a full update must be complete.

Updates are how progress happens: a working page is one turn with no inputs (§3.7), updated as the work advances, so that every attached surface — and any that attach midway — shows the same state.

3.10.1 Folding #

The daemon MUST fold every update into its stored copy of the outstanding turn, and the turn it sends a late joiner (§3.6) is that folded state. An update is therefore transient on the wire but never lost: attaching and having-been-attached see the same page.

A surface receiving an update for a seq it does not hold MUST ignore it — the TURN that supersedes it is already on the wire. A sparse update naming a ref the surface does not know is a daemon defect; the surface SHOULD fail the conversation rather than guess.

3.11 Ending and Errors

Peios / Peios technical reference / PGSS / MSIP

3.11.1 End #

A conversation ends exactly one way: the daemon broadcasts End.

FieldMeaning
seqThe conversation's final seq.
outcomecomplete, cancelled, or failed.
messageOptional text for the person.

complete is the flow's own success. cancelled reports a decision — a person pressed the action the daemon interprets as abandonment, or daemon policy expired an abandoned conversation (§3.3). failed reports that the work cannot continue. In every case the daemon SHOULD have said more in the preceding turns; message is a last line, not a report.

After END the conversation is over: its identifier MUST NOT appear in a listing, an Attach naming it is refused unknown_kind, and the daemon SHOULD close the connections that were attached. A surface receiving END renders the outcome and is done.

3.11.2 Error #

Error reports a protocol violation on one connection, and nothing else — it is never a flow outcome, never validation, never staleness.

codeViolation
malformedUndecodable body, or a field breaking this chapter's rules.
unboundA message requiring a binding, on an unbound connection — or a second binding attempt on a bound one.
unknown_refAn answer naming a ref the turn does not have.
bad_valueA value on an action/disabled/output element, a wrongly-typed value, or an action rule of §3.9 broken.
unexpected_typeA message type this peer never receives, or one invalid in the connection's state.

An Error is advisory: the sender MAY close the connection after it, and MUST close it when the violation makes further decoding unsafe. Errors are per-connection; the conversation, and every other surface, are unaffected.

3.12 Conformance

Peios / Peios technical reference / PGSS / MSIP

3.12.1 A conforming daemon #

  • Offers its conversations on a socket whose access control is a security descriptor, and checks the connection's verified peer identity at START, ATTACH and LIST (§3.4).
  • Assigns conversation identifiers of 128 cryptographically random bits, and treats the ATTACH access check — not identifier secrecy — as the control (§3.6).
  • Never sends an element whose type the connection's HELLO did not declare, and refuses a binding the surface cannot render, unsupported_elements, at bind time (§3.6).
  • Broadcasts every TURN, UPDATE and END to every attached connection, and sends a new attachment the outstanding turn in its folded state (§3.3, §3.10).
  • Applies the first valid answer; keeps the turn open on validation failure, broadcasting the errors; ignores stale answers silently; answers protocol violations with Error on the offending connection only (§3.9, §3.11).
  • Increments seq by one per turn and never rewinds it (§3.7).
  • Keeps every secret value out of element state, updates, errors, listings and logs (§3.4).
  • Does not vary behaviour on Hello.surface, or on anything else this chapter marks presentational (§3.4).
  • Documents, in its own specification, its conversation kinds, their refs and answer types, and its multiplicity policy per kind.

3.12.2 A conforming surface #

  • Sends HELLO first, declaring exactly the element types it can render, and fails rather than guesses if it is nevertheless sent something it cannot render (§3.4, §3.6).
  • Renders the elements of the outstanding turn, honouring enabled, secret and the input/output/action division; collects no input for disabled or output elements (§3.8).
  • Answers only the outstanding seq, names exactly one enabled action when the turn offers any and none when it offers none, and sends values only for enabled input elements (§3.9).
  • Applies updates by folding, ignores updates for turns it does not hold, and re-renders from scratch on a full update (§3.10).
  • Treats names, help, messages and classes as text for the person, never as instructions to itself (§3.4).
  • Keeps secrets unechoed, unlogged and unretained (§3.4).

Appendix 3.A Message Reference

Peios / Peios technical reference / PGSS / MSIP

Message type values. Surface-sent types have the high bit clear; daemon-sent types have it set (0x8000).

MessageDirectionmsg_type
Hellosurface → daemon0x0001
Listsurface → daemon0x0002
Startsurface → daemon0x0003
Attachsurface → daemon0x0004
Answersurface → daemon0x0005
Welcomedaemon → surface0x8001
Listingdaemon → surface0x8002
Bounddaemon → surface0x8003
Refuseddaemon → surface0x8004
Turndaemon → surface0x8005
Updatedaemon → surface0x8006
Errordaemon → surface0x8007
Enddaemon → surface0x8008

Field tables. A field marked opt may be absent; its default is given in the section that specifies the message.

Hello (§3.6): element_types (array of string), surface (opt string).

Welcome (§3.6): kinds (array of string), daemon (opt string).

List (§3.6): no fields.

Listing (§3.6): conversations — array of {conversation, kind, seq}.

Start (§3.6): kind (string).

Attach (§3.6): conversation (string, 32 lowercase hex).

Bound (§3.6): conversation (string), seq (number), attached (opt bool, default false).

Refused (§3.6): reason (access_denied | unknown_kind | unsupported_elements | unavailable), message (opt string).

Turn (§3.7): seq (number), id (opt string), name (opt string), class (opt array of string), elements (array of element objects, §3.8).

Update (§3.10): seq (number), full (opt bool, default false), elements (array of objects: patches when sparse, complete elements when full).

Answer (§3.9): seq (number), action (opt string), values (opt object, ref → value).

Error (§3.11): code (malformed | unbound | unknown_ref | bad_value | unexpected_type), message (opt string).

End (§3.11): seq (number), outcome (complete | cancelled | failed), message (opt string).

Appendix 3.B Element Types

Peios / Peios technical reference / PGSS / MSIP

The initial set. Each type lists its state fields (beyond the common fields of §3.8) and, for input types, the JSON encoding of its answer value. The set grows by capability negotiation, not by version bump (§3.5); a new type is specified by adding it here.

3.B.1 text — output #

Prose for the person to read.

StateMeaning
textThe content.
detailWhat the content says, as structure. Optional; see Detail, at the end of this chapter.

3.B.2 string — input #

One line of text; with secret, a password collector.

StateMeaning
min, maxLength bounds, in characters.
patternAnchored regular expression the value must match.
placeholderGhost text; not a default.

Answer value: JSON string.

3.B.3 boolean — input #

A yes-or-no.

Answer value: JSON true/false.

3.B.4 select — input #

Choose from what the daemon offers.

StateMeaning
choicesArray of {value, name, help?}, in display order.
multipleZero or more choices rather than exactly one.

Answer value: one choice's value; with multiple, an array of them. Values are compared as exact JSON values.

3.B.5 table — input #

Choose among rows that have more to say than a name: a select whose choices are records. Disks, network interfaces, existing installations — anything a person compares on several facts before picking one.

StateMeaning
columnsArray of {key, name, align?}, in display order. align is left (default) or right; numbers and sizes read better right-aligned.
rowsArray of {value, cells, enabled?, note?, detail?}, in display order. cells maps a column key to display text; a key a row omits renders empty. enabled defaults to true; a false row is listed but is not an acceptable answer. note is a short remark shown with the row — typically why it is disabled. detail is what the row stands for, as structure; see Detail, at the end of this chapter.
emptyText shown in place of the rows when there are none.

Answer value: one enabled row's value. Values are compared as exact JSON values.

A surface that cannot fit every column SHOULD drop columns from the right rather than truncate every cell, so the daemon's column order is also its priority order.

3.B.6 progress — output #

How far along the work is.

StateMeaning
valueProgress so far.
maxThe whole; absent means indeterminate.

3.B.7 log — output #

Accumulating lines of detail.

StateMeaning
linesThe lines so far.
appendIn a sparse patch only: lines to add.

append exists so progress detail need not resend the backlog; the daemon folds appended lines into lines (§3.10), so a late joiner receives the accumulated state.

3.B.8 action — pressed #

StateMeaning
validateDefault true; false skips §3.9 validation.
primaryThe action an unattended answerer presses. At most one per turn.
destructivePressing this destroys something; a surface SHOULD make it hard to press by accident.

Answer value: none, ever. The pressed action travels in Answer.action.

3.B.9 Detail #

A table row and a text element may each carry a detail: an object of further facts about what the words stand for, as structure rather than as text. A row's cells say "931.5 GiB" and nothing of what is on the disk; its detail gives the size in bytes and the partitions. A sentence says the whole disk will be erased, Windows included; its detail gives the disk it is about.

It exists for a surface that knows the page by its template id (§3.2) and draws it as more than its elements. Its keys are the daemon's, documented with the daemon's refs; this chapter defines none. A surface that does not know them MUST ignore detail, and a daemon MUST NOT put there anything a person needs in order to answer: a row's cells and note, and a text's words, remain everything they say to a surface that draws them as they are.

Appendix 3.C Prior Art

Peios / Peios technical reference / PGSS / MSIP

3.C.1 PAM, via PGSS Logon #

The conversation shape — a privileged party that asks for what it needs, an untrusted renderer that collects without understanding, and capability negotiation so nothing unrenderable is ever sent — is PAM's, adopted into this document once already by Logon (§2). Logon's credential exchange is in this sense a specialisation of MSIP: a single-kind, single-surface conversation whose elements are prompts. The two chapters remain separate protocols on separate sockets — a logon conversation is one connection with no attachment, no persistence and no broadcast, and gains from that austerity (§2.3) — but a reader will recognise the shape, and divergence between the two should be deliberate.

3.C.2 Debconf #

Debconf solved this chapter's exact problem — one engine, many frontends — and proved the central discipline: a tiny, semantic vocabulary of question types, with rendering wholly owned by the frontend. It also demonstrates the failure mode this chapter's §3.7 legislates against: its templates grew presentation-shaped extensions the moment a frontend could exploit them. What MSIP does not adopt is the shared question database and its state machine; a conversation is owned by its daemon, not by a global store.

3.C.3 Subiquity #

Ubuntu's installer is the closest modern relative: a privileged server driving replaceable clients (TUI, web) over a wire protocol, with attach-and-resume. It validates the process model — and warns about the API: its per-screen endpoints make each page a bespoke contract, so every new page changes both sides. MSIP's turn/element model is the generalisation that keeps the wire contract fixed while pages change.

3.C.4 Desktop agent protocols #

polkit's authentication agents and systemd's ask-password watchers are the daemon-has-a-need pattern: a session registers a renderer, and privileged software asks through it. §3.3's agent-style conversation is that pattern expressed without a second protocol — the surface still opens; the daemon still drives.

3.C.5 Design influences #

Broadcast with first-valid-answer arbitration is what a conversation that outlives connections forces: once two surfaces can watch, choosing a "driving" surface means takeover semantics and detach protocols; arbitration by seq needs neither, and turn sequence numbers were already required for staleness.

Refs versus seqs — semantic identity apart from sequence — is the split that keeps MSIP a conversation rather than a form language. A stable question list would be a schema; stable refs on an unpredictable path is a dialogue.

4.1 Scope and Roles

Peios / Peios technical reference / PGSS / Icons

This chapter specifies PGSS Icons: how a program, a file, a directory or a kind of thing on a Peios system is given an icon, and where the icon comes from.

The chapter rests on one decision. An icon is a name, not a picture. Every party that has something to say about what a thing looks like says a name, and a name becomes a picture in one place only: the theme chain (§4.4). A program carries its name in its own file; a file's owner may set one on it; a file's own name and its type each yield one. Whoever draws the thing looks the name up, and a theme that provides the same name decides how it is drawn.

Two roles participate.

The provider puts icons where they can be found, and says which one a thing has. A package that installs an icon into a theme is a provider. So is a program that carries the name of its icon, and so is a person who sets a name on a file they own.

The consumer draws. Given a thing, it finds the name by the rules of §4.5 and the picture by the rules of §4.4, and draws what it finds. A launcher, a taskbar, a file explorer and a compositor drawing window frames are all consumers, and a provider cannot tell them apart.

Both roles are publicly implementable. A third party MAY ship a theme, a program carrying an icon, or a consumer, and interoperate with the rest unchanged. A consumer that draws icons for things it did not make MUST find them as specified here, precisely so that a provider need say what a thing looks like once, to everyone.

This chapter covers:

  • what a name is, and which names mean what (§4.3)
  • what a theme is, where themes are, and how a name becomes a file (§4.4)
  • how a consumer finds the name for a file: the sources it consults and the order it consults them in (§4.5)
  • how a file's type yields a name (§4.6)
  • the names every system has (§4.A)

This chapter does not cover:

  • What a program is to a desktop: its title, what it opens, how it is started. That is PGSS Applications (§5), which gives a program an icon by name under §4.5.
  • How a consumer determines a file's type. §4.6 takes the type as given.
  • How a consumer draws a picture, at what size, or in what colour. Rendering is the consumer's.
  • Pictures that are not icons: a thumbnail of a photograph, a preview of a document. A consumer that shows those shows them instead of an icon, by rules of its own.

4.2 Terminology

Peios / Peios technical reference / PGSS / Icons

Icon. A picture that stands for a thing: a program, a file, a kind of file, a directory. In this chapter an icon is always referred to by its identifier, and the picture is whatever the theme chain gives that identifier.

Identifier. The name of an icon: a short string in the form of §4.3. generic, inode-directory and dev.peios.gxwi.hello are identifiers.

Theme. A directory holding one picture per identifier it provides, in the form of §4.4. A theme provides an identifier if the directory holds a file named for it.

Base theme. The theme every system has, on which every other stands. It is where a program's own icon is installed, and it provides every identifier of §4.A.

Chain. The ordered list of themes a consumer looks in. The chain is the system's, not the consumer's: it is set as §4.4 says, and it always ends in the base theme.

Subject. The thing a consumer wants an icon for: a path on the filesystem, usually. §4.5 gives the rules for a path; a specification that gives icons to other kinds of subject says how they yield an identifier.

Source. One place an identifier for a subject may come from: the file's extended attribute, the section a program carries, the file's name, the file's type. §4.5 lists the sources and the order they are consulted in.

Terms defined in PCDS (security descriptor) and in the Peios Kernel TRM (extended attribute) are used with the same meaning and are not redefined. Media type is used as RFC 6838 defines it.

4.3 Identifiers

Peios / Peios technical reference / PGSS / Icons

An identifier is a string of 1 to 255 bytes, each a lowercase ASCII letter, an ASCII digit, ., _ or -, whose first byte is a letter or a digit:

identifier = first *254 rest
first      = %x61-7A / %x30-39
rest       = first / "." / "_" / "-"

A consumer MUST treat a string of any other form as no identifier: it is not looked up, and the source it came from is passed over as though it had said nothing (§4.5). This is what keeps a name from naming a path: no identifier contains /, and none begins with ..

Identifiers are compared byte for byte. There is no case folding, because there is no case.

4.3.1 Meaningful forms #

Some forms of identifier are given meaning by this chapter, and a provider MUST NOT use them for anything else:

FormMeaningDefined in
genericThe icon of last resort§4.5
directory-<name>A directory called <name>§4.5
file-<name>A file called <name>§4.5
<type>-<subtype>A file of that media type§4.6
<type>A file of any subtype of that media type§4.6

The set is closed. A future version of this chapter MAY add a form; a provider MUST NOT invent one.

4.3.2 A program's own #

A program's own icon is named for the program, in the reverse-DNS form its package is named in (PSPU §5.3): dev.peios.gxwi.hello. The package that installs the program installs the icon into the base theme under that name, and the program carries the name (§4.5).

A provider MUST NOT install an icon under a name that is another provider's, and a package manager MAY refuse a package that would. The reverse-DNS form makes that a matter of who owns the domain.

Note

The standard names of §4.A, the forms above and reverse-DNS names cannot collide: a reverse-DNS name has a dot in its first label position where the others have a hyphen or nothing. A theme can therefore provide all three kinds in one flat directory.

4.4 Themes

Peios / Peios technical reference / PGSS / Icons

A theme is a directory under /usr/share/icons/, named for the theme, holding one file per identifier it provides:

/usr/share/icons/<theme>/<identifier>.svg

A theme's name has the form of an identifier (§4.3). A theme is flat: a consumer MUST NOT look in subdirectories of a theme, and a provider MUST NOT put anything there that a consumer is meant to find.

4.4.1 Format #

A theme's file is an SVG document. This version of the chapter recognises no other format: a file with another extension is not an icon, whatever it holds.

An icon is drawn at whatever size the consumer needs, so a provider SHOULD draw for a square canvas and SHOULD make sure the picture reads at 16 pixels. A consumer draws the document as an image and MUST NOT run any script in it.

Note

Vector icons are what let a theme be one flat directory: there is no ladder of sizes to arrange, and no index is needed to find the right rung. A raster format, if a later version admits one, is an extension of the file's name and not of the theme's shape.

4.4.2 The base theme #

The base theme is the directory /usr/share/icons/base/. Every system MUST have it, and it MUST provide every identifier listed in §4.A.

The base theme is where a provider installs a program's own icon (§4.3). Another theme MAY provide the same identifier, and is then consulted first.

4.4.3 The chain #

The chain is the ordered list of themes a consumer looks in. It is set in the registry, so that every consumer on the system looks in the same places:

KeyValueTypeMeaning
Machine\Generic\IconsThemesREG_MULTI_SZTheme names, first consulted first

A consumer MUST append the base theme to whatever the value names, and MUST ignore the base theme where the value names it, so that the chain always ends in the base theme and nowhere else. An entry that is not in the form of an identifier, or names a directory that is not there, is passed over. The value absent, or the key absent, is a chain of the base theme alone.

A consumer reads the chain when it starts and MAY read it again. A change to the chain takes effect for a consumer when it next reads it.

4.4.4 Lookup #

A consumer looks an identifier up by trying each theme in the chain, in order, for a file named for the identifier, and takes the first it finds. The lookup of an identifier no theme provides fails; §4.5 says what a consumer does then.

A consumer MUST check the identifier's form before it looks it up (§4.3). The form admits no / and no leading ., so a well-formed identifier can name nothing outside the theme's directory.

4.4.5 Caching #

Nothing in this chapter requires a consumer to keep any state between lookups, and the flat form of a theme is chosen so that none is needed. A consumer that nevertheless remembers a lookup MUST notice when the theme's directory or the chain changes, or MUST say, in its own documentation, how stale a remembered result may be.

4.5 From a File to an Identifier

Peios / Peios technical reference / PGSS / Icons

Given a path, a consumer finds the identifier to draw by consulting the sources below in order, and taking the first identifier that the chain provides (§4.4). A source that yields no identifier, or one the chain does not provide, is passed over and the next is consulted. If every source is passed over, the identifier is generic, which the base theme provides (§4.A).

A consumer MUST consult the sources in this order and MUST NOT skip one.

4.5.1 A governing specification #

A specification that says what a kind of file means to a desktop MAY also say what icon it has. A consumer that implements such a specification applies it first, and if it yields an identifier the chain provides, that is the icon.

Such a specification MUST yield an identifier, to be looked up under §4.4 like any other, and MUST NOT yield a picture.

Note

This is the step by which a declaration that a program is an application, with a title and the types it opens, gives the program an icon. That declaration is PGSS Applications' (§5.5); this chapter only leaves it the first word.

4.5.2 The owner's word #

A file or directory MAY carry the extended attribute user.pgss.icon, whose value is an identifier. The attribute is the word of whoever owns the file, and is consulted second.

A consumer MUST treat a value that is not in the form of an identifier as no value. A consumer MAY treat a value ending in a NUL byte as though the NUL were absent.

Because the attribute is in the user namespace, whoever can write the file can set it, and it can be set on regular files and directories only. A file that cannot carry an attribute is drawn by the sources that follow.

4.5.3 The program's word #

A regular file that is an ELF object MAY carry a section named .pgss.icon, whose contents are an identifier, optionally followed by a NUL byte and nothing else. The section is the word of whoever built the program, and is consulted third.

A consumer finds the section by the object's section header table, by name, and takes the first section of that name. A consumer MUST NOT execute or load the object to find it, and MUST bound what it reads: a header that points outside the file, a section header table of implausible size, or a section longer than an identifier could be, is no section.

A file that is not an ELF object, or is one without the section, is drawn by the sources that follow.

Note

The section holds a name and not a picture on purpose. A program that carried its own SVG could not be restyled by a theme, and a consumer listing a directory of programs would be handed a picture by every one of them. A name is a few bytes, and it is the theme's to draw.

4.5.4 The name #

The fourth source is the file's own name. For a directory the candidate is directory- followed by the name; for anything else it is file- followed by the name; in both the name is converted to lowercase ASCII first. If the result is not in the form of an identifier, which is so for any name with a space or a non-ASCII character in it, the source yields nothing.

So a directory called Documents is drawn as directory-documents if the chain provides it, and a file called Makefile as file-makefile. Which such names a theme provides is the theme's business; a consumer MUST NOT special-case any.

4.5.5 The type #

The fifth source is the file's type, which yields identifiers as §4.6 says.

4.5.6 Symbolic links #

A consumer that shows a symbolic link as the thing it points to consults the sources for the target. A consumer that shows it as a link consults them for the link itself, and a link, being unable to carry an attribute, is then drawn by its name or its type. A link whose target does not exist is drawn as generic.

4.5.7 What an icon is not #

An icon is what a thing looks like, and is chosen by parties that can say anything. A consumer MUST NOT infer from a subject's icon what the subject is, what it will do, or who made it. A file drawn as a program is not thereby a program.

4.6 File Types

Peios / Peios technical reference / PGSS / Icons

A file's type is a media type, text/x-python, image/png, inode/directory. How a consumer determines it is outside this chapter: by the file's name, by its contents, by a table of its own, or by asking something else. This chapter says only what a type yields once known.

4.6.1 From a type to identifiers #

A media type yields two identifiers, tried in order:

  1. The whole type, <type>/<subtype>, with / replaced by -.
  2. The top-level type alone.

Any character in either that is not permitted in an identifier is replaced by -, and the result is lowercase. So image/svg+xml yields image-svg-xml and then image, and text/x-python yields text-x-python and then text. The second lets a theme draw every kind of text alike and still draw Python apart when it cares to.

A consumer MUST try both, in that order, and pass on to generic (§4.5) when the chain provides neither.

4.6.2 Types every consumer determines #

Whatever else it knows, a consumer MUST determine these:

The file isType
A directoryinode/directory
A regular file that may be executed, and nothing better is knownapplication/x-executable

"May be executed" means what it means to the system the consumer runs on; on Peios, that the file's security descriptor grants the consumer execute access.

4.6.3 Determining a type #

A consumer MAY determine a type from the file's name alone, and a consumer listing a directory usually should: reading every file to sniff its contents is the cost that a name is meant to spare. A consumer that reads contents MUST bound what it reads.

Note

There is no system-wide table of types in this version. A consumer ships its own, which is enough for icons: a type a consumer does not know is drawn as generic, and nothing is misdrawn. A shared table is a matter for whichever chapter first needs two consumers to agree on what a file is, which is more than what it looks like.

4.7 Extension

Peios / Peios technical reference / PGSS / Icons

4.7.1 Additive changes #

The following are additive, and a consumer or provider built to this version behaves correctly in their presence:

  • A new identifier in §4.A. A theme that lacks it is a theme that does not provide it, which every consumer already handles.
  • A new theme in the chain. A consumer built to this version looks in it like any other.
  • A new source in §4.5, consulted after the type. A consumer built to this version draws generic where a newer one would draw the new source's icon.
  • A new governing specification, consulted first under §4.5.

4.7.2 Changes requiring a new version #

  • A new file format in a theme (§4.4). A consumer built to this version does not find the file, so the identifier is not provided, so every subject that relies on it is misdrawn as something further down the chain.
  • A new meaningful form of identifier (§4.3), since the set is closed.
  • A change to the order of §4.5, or a source inserted before the type. Two consumers on one system would then disagree about what a file looks like.
  • A change to the form of an identifier.

4.7.3 Reserved #

The pgss.icon name, in the user extended-attribute namespace and as the suffix of a section name, is this chapter's. A specification that wants a file to carry something else MUST use another name.

4.8 Conformance

Peios / Peios technical reference / PGSS / Icons

4.8.1 A conforming consumer #

  • Treats as an identifier only a string in the form of §4.3, and looks up nothing else.
  • Looks in the themes of the chain in order, in /usr/share/icons/ and in no subdirectory of a theme, for <identifier>.svg, and takes the first (§4.4).
  • Reads the chain from Machine\Generic\Icons Themes, appends the base theme, and ignores the base theme anywhere else in the value (§4.4).
  • Draws an SVG as an image and runs nothing in it (§4.4).
  • Notices a change to a theme or the chain, or documents how stale a remembered lookup may be (§4.4).
  • For a path, consults a governing specification, then user.pgss.icon, then .pgss.icon, then the name, then the type, in that order, and draws the first identifier the chain provides, or generic (§4.5).
  • Finds .pgss.icon by the section header table, without executing or loading the object, and within bounds (§4.5).
  • Derives the name source's identifier by lowercase ASCII conversion and prefix alone, and special-cases no name (§4.5).
  • Yields from a type the whole type and then the top-level type, and determines at least a directory and an executable (§4.6).
  • Infers nothing about a subject from its icon (§4.5).

4.8.2 A conforming provider #

  • Installs a theme as a flat directory of <identifier>.svg under /usr/share/icons/<theme>/, with the theme's name in the form of an identifier (§4.4).
  • Uses a meaningful form of identifier for its meaning and no other, and a reverse-DNS name for a program's own icon (§4.3).
  • Installs a program's own icon into the base theme, under a name within a domain it owns (§4.3).
  • Carries a program's icon as an identifier in .pgss.icon, or sets one in user.pgss.icon, and never a picture (§4.5).

4.8.3 A conforming system #

  • Has the base theme at /usr/share/icons/base/, providing every identifier of §4.A (§4.4).

4.8.4 What conformance does not require #

Conformance does not require a consumer to determine any file type beyond a directory and an executable, to show thumbnails, to cache anything, or to draw an icon at any particular size. It does not require a provider to install more than the base theme.

Appendix 4.A Standard Identifiers

Peios / Peios technical reference / PGSS / Icons

The base theme MUST provide every identifier below (§4.4). Another theme MAY provide any of them. The set is closed for this version; a later version MAY add to it (§4.7).

4.A.1 Last resort #

IdentifierDrawn for
genericAnything no other source gives an icon (§4.5)

4.A.2 Types #

The identifiers the two types every consumer determines yield (§4.6), and the top-level types a consumer is most likely to determine from a name.

IdentifierDrawn for
inode-directoryA directory
application-x-executableA program
applicationA file of any other application/ type
textA file of any text/ type
imageA file of any image/ type
audioA file of any audio/ type
videoA file of any video/ type

4.A.3 Named directories #

The directories a person is most likely to keep, by the names they are most likely to have (§4.5).

IdentifierDrawn for
directory-documentsA directory called Documents
directory-downloadsA directory called Downloads
directory-picturesA directory called Pictures
directory-musicA directory called Music
directory-videosA directory called Videos

Appendix 4.B Prior Art

Peios / Peios technical reference / PGSS / Icons

4.B.1 The freedesktop icon theme and naming specifications #

This chapter is, in outline, freedesktop's: an icon is a name, a theme is a directory that gives names pictures, themes stand in a chain that ends in hicolor, and a standard vocabulary lets one theme restyle a whole desktop. What it drops is the size ladder and the index that the ladder made necessary: with one vector file per name a theme is flat, and there is no icon-theme.cache to rebuild. It also drops the Icon= line of a desktop file as the only way a program gets a name, in favour of the program carrying it.

The name source (§4.5) is not freedesktop's, which gives Documents and Downloads their icons by configured path rather than by name. By name is simpler and needs no configuration, and the editor icon themes that colour a src directory apart from a test one show what it is for.

4.B.2 Windows #

Windows puts the picture in the program: a PE file carries icon resources, a shortcut or a registry DefaultIcon value points at path,index, and the shell extracts what it finds. That gives a raw executable an icon anywhere it is copied, which this chapter keeps by having the program carry a name. It also makes every consumer a parser of executables and every listing an extraction, which is why the shell caches, and why this chapter has the program carry a name and not a picture.

MSIX moved a packaged program's icons out of the binary into image files named in the package manifest, which is the shape this chapter gives a program's own icon: a file installed under a name the program declares.

4.B.3 Design influences #

One resolver. Every source yields a name, and a name becomes a picture in one place, so that a theme restyles programs as well as folders, and so that two consumers on one system cannot disagree about what a file looks like without one of them being wrong.

The owner's word above the program's. user.pgss.icon is consulted before .pgss.icon because an attribute is set on a particular file by whoever has it, and a section is set on every copy by whoever built it. The particular overrides the general, as a custom folder icon overrides the folder icon.

A hint, not a claim. Anyone who owns a file can make it look like anything, as anyone can on any other system. The chapter says so once (§4.5) so that no consumer is built to trust it.

5.1 Scope and Roles

Peios / Peios technical reference / PGSS / Applications

This chapter specifies PGSS Applications: what a program is to a desktop on a Peios system. Its title, the icon it has, how it is started, and what kinds of file it opens.

The chapter rests on one decision. An application is a declaration, not a place. A program is not an application because of the directory it is installed in or the name it is installed under, and a desktop does not find applications by looking at programs. A package that ships an application also installs one small file that says what it is, and the directory of those files is the catalogue (§5.4). Presence is the catalogue: nothing runs at install, nothing registers, and nothing indexes.

Two roles participate.

The provider declares. A package that installs a program and its declaration is a provider.

The consumer reads the declarations and acts on them. A launcher that lists what can be started, a dock or taskbar that names a running program, a file explorer that offers what opens a file, and a compositor that starts what a launcher picks are all consumers, and a provider cannot tell them apart.

Both roles are publicly implementable. A third party MAY ship an application, or a consumer, and interoperate with the rest unchanged. A consumer that lists, names or starts applications MUST find them as specified here, precisely so that a provider need say what a program is once, to everyone.

This chapter covers:

  • what a declaration is, where it is, and what it says (§5.3)
  • what the catalogue is, and how a consumer reads it (§5.4)
  • how a consumer finds the declaration for a program it has met, running or on disk, and so the program's title and icon (§5.5)
  • how a consumer starts an application, bare or with a file (§5.6)
  • which application opens a type of file when several could: the choices a person and the machine make, where they are kept, and how a consumer finds the default from them (§5.7)

This chapter does not cover:

  • How a program that offers to set choices presents them. §5.7 says where a choice is kept and who may write it; a settings application's shape is its own.
  • Declarations of a person's own, or of a system's local layer, beyond what the catalogue's directory admits. This version has one catalogue.
  • How a consumer determines a file's type. §5.6 takes the type as given, as PGSS Icons §4.6 does.
  • What a program does once started, or how a consumer confines it. The environment a consumer starts a program in is the consumer's.
  • What a program looks like. That is PGSS Icons (§4); this chapter gives a program an icon by name, under §4.5, and says nothing about pictures.

5.2 Terminology

Peios / Peios technical reference / PGSS / Applications

Application. A program as a desktop knows it: something with a title that can be started from a launcher or opened with. In this chapter an application is always what a declaration says, and nothing is an application without one.

Declaration. The file that says what an application is, in the form of §5.3. One file declares one application.

Id. The name of an application: the reverse-DNS string the declaration's file is named for, in the form of an icon identifier (PGSS Icons §4.3). dev.peios.gexora is an id.

Catalogue. The directory holding every declaration on the system, in the form of §5.4. A consumer that wants to know what applications there are reads it.

Program. The file a declaration says to run: an absolute path. The program is what a consumer executes, and what a consumer matches a running process against (§5.5).

Title. What an application is called wherever a consumer shows it to a person.

Listed. Whether a launcher shows the application among what can be started. An application that is not listed is still an application: it can be opened with, and a running one is still named by its declaration.

Media type pattern. Either a media type, image/png, or a top-level type and *, image/*, which stands for every subtype of it. A declaration says what an application opens in these.

Candidate. An application that says it opens a file's media type, and so may open the file (§5.6).

Choice. A media type pattern and the id of the application chosen to open it, kept in the registry by a person for themselves or for the machine as a whole (§5.7).

Default. The application a consumer opens a file with when it picks one: the candidate the choices give the file's type, by the order of §5.7. A type no usable choice is for has no default.

Terms defined in PGSS Icons (identifier, chain, consumer as one who draws) are used with the same meaning and are not redefined, and nor are those of the registry (key, value, REG_SZ, CurrentUser\), which are the Peios Kernel TRM's, or security descriptor, which is PCDS's. Media type is used as RFC 6838 defines it. TOML is the format at toml.io, version 1.0.

5.3 The Declaration

Peios / Peios technical reference / PGSS / Applications

A declaration is a file in the catalogue's directory (§5.4), named for the application's id:

/usr/share/apps/<id>.toml

The file's name is the declaration's id. Nothing in the file repeats it, so the two cannot disagree.

5.3.1 The id #

An id has the form of an icon identifier (PGSS Icons §4.3): 1 to 255 bytes of lowercase ASCII letters, digits, ., _ and -, beginning with a letter or a digit. A consumer MUST treat a file whose name, before .toml, is of any other form as no declaration.

An id is reverse-DNS, within a domain the provider owns, in the form a package is named in (PSPU §5.3): the package's own name, or a name beneath it when one package ships more than one application. A provider MUST NOT install a declaration under an id in another provider's domain, and a package manager MAY refuse a package that would.

Note

The id doubles as the default name of the application's icon. PGSS Icons §4.3 has a program's own icon named for its package in the same form, so the common case, one package, one program, one icon, needs no icon line and no second name.

5.3.2 The file #

The file is a TOML document whose top-level table has these keys:

KeyTypeMeaning
titlestringWhat the application is called. One line, not empty. Required.
programstringThe file to run: an absolute path. Required.
argumentsarray of stringsWhat the program is always started with, in order, before anything else. Empty when absent.
iconstringThe identifier of its icon (PGSS Icons §4.3). The id when absent.
descriptionstringOne line on what it is for. None when absent.
opensarray of stringsThe media type patterns it opens. Empty when absent.
listedbooleanWhether a launcher lists it. true when absent.

An example, complete for a file explorer:

title = "Gexora"
description = "Browse the files on this system"
program = "/usr/bin/gexora"
opens = ["inode/directory"]

A provider MUST write a declaration that a TOML 1.0 parser accepts, and MUST NOT rely on any key not in the table above being read.

5.3.3 What a consumer makes of it #

A consumer MUST ignore a key it does not know. That is how a later version of this chapter adds to a declaration (§5.8) without breaking a consumer built to this one.

A declaration is unusable, and a consumer MUST treat it as no declaration, when any of these holds:

  • the file is not a TOML document a TOML 1.0 parser accepts
  • title is absent, is not a string, is empty or is more than one line
  • program is absent, is not a string, or is not an absolute path
  • a key in the table is present with a value of another type

A consumer SHOULD say on its own log which file was unusable and why, so that a broken package can be found, and MUST go on to the rest of the catalogue.

A declaration that is merely odd is set right, not thrown away:

  • an icon that is not in the form of an identifier is treated as absent, and the icon is the id
  • an entry in opens that is not a media type pattern is left out
  • leading and trailing whitespace on title, description and icon is not part of the value

arguments is passed as given, byte for byte, with no whitespace splitting, no quoting and no expansion (§5.6). A declaration is data, and no part of it is a command line.

Note

The program is an absolute path and not a name looked up along a PATH on purpose. A launcher runs what a package installed, and a file of the same name earlier on somebody's path is not it.

5.4 The Catalogue

Peios / Peios technical reference / PGSS / Applications

The catalogue is the directory /usr/share/apps/. Every system MUST have it, and it MUST be readable by anyone who can log on to a desktop: every consumer reads it as the person it runs for.

The catalogue is flat. A consumer MUST NOT look in subdirectories of it, and a provider MUST NOT put anything there that a consumer is meant to find. A file in it whose name does not end in .toml, or whose name before .toml is not in the form of an id (§5.3), is not a declaration and MUST be passed over.

5.4.1 Presence is the catalogue #

A declaration is in the catalogue when its file is in the directory, and out of it when the file is gone. Nothing else is required of a provider: no command at install, no index to rebuild, no notice to any consumer. A package that installs a declaration has shipped an application, and one that removes it has withdrawn it.

A consumer MUST NOT require a provider to do anything beyond installing the file.

5.4.2 Reading it #

A consumer that needs the list reads the directory, and reads each declaration by the rules of §5.3. A consumer that needs one application by id MAY read that file alone, at <id>.toml, after checking the id's form: the form admits no / and no leading ., so a well-formed id can name nothing outside the directory.

A consumer SHOULD leave out of any list a declaration whose program is not a regular file. Nothing is there to start, and a package that is half installed or half removed should not be offered.

A consumer MAY order what it lists however it likes; a provider MUST NOT rely on any order.

5.4.3 Caching #

Nothing in this chapter requires a consumer to keep any state between reads, and a catalogue of a few hundred files is meant to be read whenever a launcher opens. A consumer that nevertheless remembers what it read MUST notice when the directory changes, or MUST say, in its own documentation, how stale a remembered result may be.

Note

This is the shape of PGSS Icons' theme (§4.4): a flat directory that is its own index. The desktop-file databases that other systems rebuild after every install exist because those catalogues could be anywhere and hold anything; a catalogue that is one directory of small files needs none.

5.5 From a Program to its Declaration

Peios / Peios technical reference / PGSS / Applications

A consumer often meets a program before it meets a declaration: a compositor sees a process connect, a taskbar is told of its windows, a file explorer lists an executable. This section says how such a consumer finds the declaration, if there is one, and what it gets from it.

5.5.1 Matching #

A program matches a declaration when the program's file and the declaration's program are the same file. Two paths name the same file when they resolve, through every symbolic link, to the same path, or when they refer to the same inode on the same device. The second is for a system that presents one file at more than one path, as Peios' merged views do (/bin over /usr/bin).

A consumer MUST match by file and MUST NOT match by name: a file called gexora anywhere is not thereby Gexora.

For a running process, the program's file is the executable the process is running, which on Peios is reached through /proc/<pid>/exe. A consumer SHOULD match through the process rather than through the path the process was started by, since the path may since have been replaced.

When more than one declaration names the same program, the consumer takes the one whose id sorts first, byte by byte. A provider SHOULD NOT declare one program twice.

5.5.2 What the match gives #

A program that matches a declaration is that application, and a consumer showing it to a person SHOULD show the declaration's title where it names the program as a whole, such as under a dock's icon, whatever its windows are called. A window keeps its own title.

The declaration gives the program its icon. Under PGSS Icons §4.5, this chapter is a governing specification, and the identifier it yields for a program is the declaration's icon. A consumer that implements both chapters consults it first: if the chain provides the identifier, that is the icon, and if not, the consumer goes on to the sources §4.5 lists after it. A program that matches no declaration is drawn by those sources alone.

A consumer MUST NOT infer from a match anything the declaration does not say. In particular, a declaration says nothing about who built the program or whether it is trusted; it says what a package chose to call it.

5.6 Starting and Opening

Peios / Peios technical reference / PGSS / Applications

5.6.1 Starting #

To start an application, a consumer executes its program with its arguments, in order, as the process's arguments after the program's own name. Each argument is passed as one argument, byte for byte. A consumer MUST NOT pass the program or the arguments through a shell, MUST NOT split, join, quote or expand them, and MUST NOT look the program up along a path: it is absolute.

What else the process is started with, its environment, its working directory, its standard streams, what it is confined by, is the consumer's. A compositor puts its display in the environment so that the program can be an app; a consumer of another kind does whatever its kind does.

A consumer MAY let a person add arguments of their own, as a launcher does with the words typed after the application's name. They follow the declaration's.

5.6.2 Opening #

To open a file with an application, a consumer starts it as above with one more argument last: the file's path, absolute. A file's path is never put anywhere but last, and there is no placeholder syntax in this version: a declaration whose arguments contains %f or {file} has an argument that is those characters.

An application whose opens contains a pattern that a file's media type matches is a candidate to open the file. A media type matches type/subtype when it is equal to it, ignoring case, and matches type/* when its top-level type is type. How the consumer knows the file's type is its own, as in PGSS Icons §4.6.

A consumer that opens a file offers the candidates, or picks one of them; which it does is the consumer's. A consumer that picks MUST pick the file's default when its type has one (§5.7), and picks by a rule of its own only when it has none. A consumer that offers SHOULD offer the default first, or otherwise show which it is. A consumer MUST NOT read a default from anywhere §5.7 does not name.

An application with no opens is a candidate for nothing, and is started bare.

5.6.3 From the catalogue, not from the request #

A consumer often starts an application because something asked it to, and the something is not always trustworthy: a page in a browser, a message from another process. A consumer that starts an application on a request that names it by id MUST take the program and the arguments from the catalogue as it stands at that moment, and MUST NOT take a program or arguments from the request. The request says which application; the declaration says what runs.

A request that names an id the catalogue has no usable declaration for starts nothing.

Note

This is why a declaration is data and never a command line, and why the program is matched by file (§5.5) and named by absolute path (§5.3). A consumer that follows this section cannot be made to run anything a package did not install, whatever it is asked.

5.7 Defaults

Peios / Peios technical reference / PGSS / Applications

A person may choose which application opens a type of file, and so may whoever administers the machine, for everyone on it. Each such choice is one value in the registry. The default for a media type is the application the choices give it, by the rules of this section, and it is what a consumer that picks among the candidates (§5.6) opens a file with.

5.7.1 Where choices are kept #

There are two places, each a registry key:

KeyWhose choices
CurrentUser\Generic\Applications\DefaultsThe person's. The kernel routes CurrentUser\ to the caller's own Users\<SID>\, so each person has their own.
Machine\Generic\Applications\DefaultsThe machine's: everyone's on it, for whatever they have not chosen themselves.

Each value of either key is one choice:

PartIs
NameA media type pattern (§5.2): the type the choice is for, image/png, or a top-level type and *, image/*.
TypeREG_SZ.
DataThe id (§5.3) of the application chosen, in UTF-8, with or without a terminating NUL.

The registry compares value names ignoring case, as media types are compared (§5.6), so one key holds at most one choice for a type however it is spelt. A writer SHOULD write the name in lowercase. A consumer reads the name ignoring case, and the data with leading and trailing whitespace removed.

A value whose name is not a media type pattern, whose type is not REG_SZ, or whose data is not in the form of an id is no choice. A consumer passes over it, SHOULD say on its own log which value it passed over and why, and goes on to the rest of the key.

A key that is absent has no choices, and neither has one the consumer may not read. Neither is an error. Anything in either key other than its values, such as a subkey, is reserved (§5.8) and a consumer MUST ignore it.

The person's choices are those of the person the file is opened for. A consumer that runs as that person reads them through CurrentUser\. A consumer that runs as anyone else MUST NOT read its own CurrentUser\ for them, since that is its own key and not the person's; it reads the person's Users\<SID>\Generic\Applications\Defaults if it may, and otherwise uses the machine's choices alone.

5.7.2 Finding the default #

The default for a file whose media type is type/subtype is found by trying these choices in order and taking the first that is usable:

  1. the person's choice named type/subtype
  2. the person's choice named type/*
  3. the machine's choice named type/subtype
  4. the machine's choice named type/*

A choice is usable for the file when the catalogue has a usable declaration for the id it names (§5.3, §5.4), and that application is a candidate to open the file (§5.6). A choice that is not usable is passed over and the next is tried. It is never an error: an application that has been removed, or that no longer says it opens the type, is simply no longer anybody's default, and nothing need be done about the choice that named it. A consumer MAY say on its own log that it passed one over.

When no choice is usable, the type has no default, and a consumer that picks uses its own rule among the candidates (§5.6).

A choice selects among the candidates and never adds one. A consumer MUST NOT open a file with an application that is not a candidate for it, whatever a choice says.

Note

The person's choices come before the machine's whichever is the more particular. A person who chooses one application for image/* means every image, including the types the machine has chosen something else for; the machine's choices are what a person has not decided, not what overrules them.

A consumer reads the choices when it picks, or remembers them under the rule of §5.4 for a remembered catalogue: it MUST notice when either key changes, or MUST say, in its own documentation, how stale a remembered choice may be.

5.7.3 Setting them #

A person sets their own choices by writing values in their own key, and clears one by deleting its value. Whoever may write the machine's key sets the machine's in the same way. A writer creates each key of the path that is not there yet, one level at a time: the registry creates no key on the way to another.

Who may write each key is that key's security descriptor, and nothing else. A system MUST let every person who can log on to a desktop read the machine's key, and SHOULD let only administrators write it, as a Peios system's Machine hive does from its root: SYSTEM and Administrators have full control, and Authenticated Users read. A person's own key is theirs by the descriptor of their part of the Users hive.

A program that offers to set choices, such as a settings application, MUST find out whether it may write a key by asking the registry: by opening the key for writing values, or, where the key is not there yet, the nearest key above it that is for creating subkeys. It MUST NOT infer the answer from the groups the person is in, since the descriptor, and not membership, is what the registry checks.

A writer SHOULD offer, for a type, only the applications that are candidates for it, and for a pattern type/*, only those that say they open type/*. It is not an error to write anything else: a choice that is not usable is passed over when it is read.

Note

Any program running as a person can change that person's choices, as it can change anything else of theirs. This section does not try to prevent it, since a program running as the person could as easily do whatever it would have done by being the default. What it does prevent is a choice opening a file with something that is not a candidate, or with something a package did not install.

5.8 Extension

Peios / Peios technical reference / PGSS / Applications

5.8.1 Additive changes #

The following are additive, and a consumer or provider built to this version behaves correctly in their presence:

  • A new key in a declaration (§5.3). A consumer built to this version ignores it, and a declaration that carries it is usable by that consumer as far as the keys it knows go. A new key MUST be optional, and its absence MUST mean what this version's consumers do.
  • A new kind of consumer. A provider has nothing to do for it.
  • A new declaration in the catalogue, and a new provider.
  • A second place for declarations, consulted after the catalogue, such as a person's own. A consumer built to this version does not see it, and shows the system's applications where a newer one would show more.
  • A subkey of a key of §5.7, or a new key beside Defaults under either Applications key. A consumer built to this version ignores them, and finds the same defaults as before.

5.8.2 Changes requiring a new version #

  • A change to the catalogue's directory, or a subdirectory a consumer is meant to look in (§5.4). A consumer built to this version does not find what is there.
  • A new file format or extension for a declaration (§5.3).
  • A meaning for any sequence of characters in arguments, such as a placeholder for a file. A consumer built to this version passes it through as it is, so an application that relied on it would be started wrongly.
  • A place for the file's path other than last (§5.6).
  • A change to what makes a declaration unusable (§5.3), or to the match rule (§5.5). Two consumers on one system would then disagree about what is installed, or what a program is.
  • A change to where choices are kept, to the form of a choice, or to the order they are tried in (§5.7), including a place for choices consulted between the person's and the machine's, such as a group's. Two consumers on one system would then open one file with different applications.

5.8.3 Reserved #

The directory /usr/share/apps/ is this chapter's, and so is the .toml extension within it. The keys named in §5.3 are this chapter's: a specification or a provider that wants a declaration to carry something else MUST use another key, and SHOULD prefix it with a domain it owns.

The registry keys Machine\Generic\Applications and CurrentUser\Generic\Applications, and everything in them, are this chapter's. Nothing else may keep anything there.

5.9 Conformance

Peios / Peios technical reference / PGSS / Applications

5.9.1 A conforming consumer #

  • Reads declarations from /usr/share/apps/ and no subdirectory of it, from files named <id>.toml with the id in the form of §5.3, and passes over any other file (§5.4).
  • Requires nothing of a provider beyond the file's presence (§5.4).
  • Reads a declaration by the rules of §5.3: takes the keys it knows, ignores the rest, treats an unusable declaration as none and goes on, and sets an odd one right as §5.3 says.
  • Checks an id's form before making it a path (§5.4).
  • Notices a change to the catalogue, or documents how stale a remembered read may be (§5.4).
  • Matches a program to a declaration by file, never by name, and through the process for a running one (§5.5).
  • Yields the declaration's icon as the identifier for the program under PGSS Icons §4.5, and goes on to §4.5's other sources when the chain does not provide it (§5.5).
  • Starts an application by executing its program with its arguments as given, without a shell, splitting, quoting, expansion or path lookup, and with a file's path last (§5.6).
  • Takes the program and arguments from the catalogue, never from a request that names an id (§5.6).
  • When it picks among the candidates, picks the default where the type has one, and offers it first, or shows which it is, when it offers them (§5.6).
  • Finds the default from the person's choices and then the machine's, each for the exact type and then for its top-level type and *, and passes over a choice that is malformed or not usable without treating it as an error (§5.7).
  • Reads the person's choices as the person the file is opened for, and never through a CurrentUser\ of its own on their behalf (§5.7).
  • Opens a file with nothing that is not a candidate for it, whatever a choice says, and reads a default from nowhere but §5.7's keys (§5.6, §5.7).
  • When it offers to set choices, finds out whether it may by asking the registry, never by the person's groups (§5.7).
  • Infers nothing about a program from a match beyond what the declaration says (§5.5).

5.9.2 A conforming provider #

  • Installs a declaration as /usr/share/apps/<id>.toml, with the id in the form of §5.3 and within a domain it owns.
  • Writes a TOML 1.0 document with a one-line title and an absolute program, and relies on no key outside the table of §5.3.
  • Does not rely on any order of listing, on any consumer's reading a key it does not know, or on any placeholder in arguments (§5.3, §5.6).
  • Does not declare one program twice (§5.5).

5.9.3 A conforming system #

  • Has the catalogue at /usr/share/apps/, readable by anyone who can log on to a desktop (§5.4).
  • Lets anyone who can log on to a desktop read Machine\Generic\Applications\Defaults where it is present, and SHOULD let only administrators write it (§5.7).

5.9.4 What conformance does not require #

Conformance does not require a consumer to list applications in any order, to show a description, to let a person add arguments, to cache anything, to determine any file's type, or to offer to set choices. It does not require a provider to declare every program it installs: a program with no declaration is a program, and is still drawn and run as one.

Appendix 5.A Prior Art

Peios / Peios technical reference / PGSS / Applications

5.A.1 The freedesktop desktop entry specification #

This chapter is, in outline, a desktop entry: one small file per application, installed by its package into a shared directory, with a name, an icon, an executable and the MIME types it handles. title is Name, icon is Icon, opens is MimeType, and listed = false is NoDisplay=true without the double negative.

What it drops is Exec. A desktop entry's Exec is a command line with its own quoting rules and a set of field codes, %f, %F, %u, %U, %i, %c, %k, that every consumer must parse and expand, and that have been the source of most of the specification's errata and several of its security advisories. This chapter has program, an absolute path, and arguments, an array that is passed as it is, with the file last. There is nothing to parse and nothing to expand.

It also drops Categories, Keywords, Actions, TryExec, Terminal, StartupNotify and the localised forms of every key, along with update-desktop-database and the mimeinfo.cache it builds. A first version needs a title, a program, an icon and the types it opens, and a directory that is its own index.

5.A.2 Windows #

Windows has three answers to what an application is, and this chapter takes something from each. A Start Menu shortcut is a file in a shared directory whose presence is the listing, which is the shape of §5.4. App Paths in the registry lets a name resolve to an absolute path without a PATH lookup, which is why program is absolute. Registered applications and Default Programs let a program say what it opens, which is opens. The default Windows keeps beside it, a machine's in HKEY_LOCAL_MACHINE and a person's own UserChoice under HKEY_CURRENT_USER, is the shape of §5.7: a person's choice before the machine's, each a registry value.

What this chapter does not take is the registry as the place for declarations. Peios has a registry and does not use it for them, because a package installs files, and a file is present or it is not: there is no second thing to keep consistent with the first. A choice is another kind of thing. It is not installed by a package but made by a person, it belongs to someone, and who may change it is a question of access control. Keeping who-may-change-what is what the registry is for, and CurrentUser\ gives each person their own place without a file in their home directory for every consumer to find.

Windows protects UserChoice with a hash, so that a program cannot quietly make itself a person's default. §5.7 does not: a program running as a person can already do whatever it would have done by being the default, so the hash protects little that the person's own token does not already give away. What a choice can never do here is add a candidate, or run anything a package did not install.

5.A.3 freedesktop MIME applications #

The freedesktop MIME applications specification keeps defaults in mimeapps.list files, a person's in their configuration directory before the system's, under [Default Applications], with a list of applications per type to try in order. §5.7 keeps the order, the person's before the system's, and takes one application per choice rather than a list: a choice that is not usable falls through to the machine's, which is the second entry such a list exists for. It drops the [Added Associations] and [Removed Associations] sections, which let a person make an application a candidate for a type it does not declare, or stop it being one; here a candidate is what a declaration says, and nothing a person keeps can change it.

5.A.4 MSIX #

An MSIX manifest declares an application's entry point, display name and visual assets in one document the package ships, and the package manager reads it at install. That is the shape of a declaration, kept as a file the package installs rather than something the package manager extracts, so that presence is enough.

5.A.5 Atrium #

Atrium was Peios' previous web shell and has been completely replaced by GXWI. Its historical application catalogue influenced this chapter's "presence is the catalogue" rule: installing a declaration is enough; nothing registers at install time.

This chapter carries forward TOML declarations and names derived from the installed file, rather than a separate id key. The former Atrium model loaded a web document from an application directory; PGSS Applications declares a program to run and an icon name for PGSS Icons to resolve.

5.A.6 Design influences #

A declaration, not a place. An application is what a file says it is, and not where a binary sits. A program can be a command and an application at once, a wrapper or a script can be an application, and nothing about a program's path has to change for it to become one.

Matched by file, named by path. A running process is matched to its declaration by the file it runs, and never by its name, so that no program can borrow another's title by being called the same thing. A declaration names its program absolutely for the same reason.

Data, never a command line. The one thing every consumer does with a declaration is start a process from it, often because something untrustworthy asked. Passing the program and arguments through exactly as installed is what makes that safe, and is why there are no placeholders, no shell and no lookup.

One directory that is its own index. As with PGSS Icons' themes, the flat directory is chosen so that no state has to be built, rebuilt or kept consistent, and so that a package's install and remove are the whole of the provider's work.

A choice selects; it never adds. What may open a file is what declarations say, and a person's or the machine's choice only says which of those comes first. A choice that no longer fits is passed over rather than repaired, so that removing an application, or a package changing what it opens, needs nobody to tidy the registry after it.

6.1 Scope and Roles

Peios / Peios technical reference / PGSS / Events

This chapter specifies PGSS Events: what an event on a Peios system is called, what its payload carries and how, when it is written at all, and how the vocabulary is catalogued.

The chapter rests on one decision. A name is policy. Every dot in an event type is a place an administrator can attach a security descriptor, and every field in a payload is an object an access check can grant or deny. Two spellings of one value are two objects, and a descriptor that denies one does not deny the other. So the names in this chapter are fixed with the same care as an access mask, and each value has exactly one of them.

Two roles participate.

RoleObligation
EmitterWrites events. Everything an event is named, carries and encodes is an emitter obligation, as is deciding whether to write it at all (§6.9) and describing it in a fragment (§6.10).
ConsumerReads events, or the catalogue that describes them. A store, a viewer, a forwarder and a policy editor are all consumers. Every rule about interpreting a name or a value binds the consumer.

A kernel subsystem is an emitter, and so is a daemon, a service and an application. The emitter role does not depend on the path an event takes to the stream: an event a daemon writes through the emission system calls and an event the kernel writes on a daemon's behalf (§6.7) are both held to this chapter.

Both roles are publicly implementable. A third party MAY ship an emitter or a consumer and interoperate with the rest unchanged. That is the reason the vocabulary is specified rather than left to each component: a consumer that searches for a principal searches every emitter's events with one query, and an emitter that names its fields as specified is found by every consumer without either knowing the other.

This chapter covers:

  • the form of an event type, and who may use which root (§6.3)
  • the form of a field path, the roles and domains a path leads with, and the names under which a consumer exposes the record header (§6.4)
  • the encoding of every kind of value (§6.5)
  • how an event records success and failure (§6.6)
  • how access decisions are recorded, including those a userspace component makes about objects it guards (§6.7)
  • what is an event rather than a log line or a metric, and the tier each event declares (§6.8)
  • the emission policy that decides whether an event type is written at all (§6.9)
  • the catalogue of event and field definitions, and the rules a definition must pass (§6.10)

This chapter does not cover:

  • The record format, the header layout and the ring-buffer protocol a consumer follows to drain events. These are PSPK §2.
  • How a process or kernel subsystem writes an event to the stream. The emission interfaces are described in the Peios Kernel TRM, and the libraries that wrap them in the SDK documentation.
  • Storing, retaining and querying events, and per-field read access. The query language is PSPU §3; one implementation's storage and retention are described in the eventd TRM.
  • The meaning of each event and field. That is reference material, generated from the catalogue (§6.10) into the Events reference book.
  • Log lines and metric samples, which are PSPU §3's. This chapter says only how to tell them from events (§6.8).
  • How the kernel's access-check interface accepts and copies an audit context. This chapter says what a component passes (§6.7); the interface is described in the Peios Kernel TRM.

6.2 Terminology

Peios / Peios technical reference / PGSS / Events

Event type. The dotted name an event is written under, in the form of §6.3. kacs.audit.access.checked is an event type.

Segment. One dot-separated part of an event type or of a field path.

Root. The leading segments of an event type that name its owner: a platform root, or a package name (§6.3).

Platform root. A short root reserved for one platform component, listed in §6.A.

Field. One value in an event, named by its path. A field is either in the payload or, for the header fields of §6.4, in the record header.

Path. The dotted name of a field. In a payload, each segment is a map key and the path is the chain of maps leading to the value (§6.4).

Role. The leading segment of a path that names a participant in the event: subject, object, source, destination or emitter (§6.4).

Participant. A party to an event that a role names: who acted, what was acted on, where something came from or went to, and who wrote the record.

Domain. The leading segment of a path that is not a role. A domain names a property of the event itself, such as outcome, or a thing the event happened within, such as a ring buffer (§6.4).

Thing. The segment after a role that says what kind of participant it is: token, process, file, key.

Variant. A field that is another reading of a value with its own field: the value before a change, the value asked for, the value expected. A variant is named by a qualifier (§6.4).

Fragment. A file describing events and fields, in the form of §6.10. The catalogue is every fragment installed on a system.

Tier. How much an event matters to an operator, declared per event type: essential, standard, verbose or debug (§6.8).

Essential. The tier of an event that is never switched off (§6.8).

Emission policy. The registry tree that switches event types on and off (§6.9).

Gating. What decides whether one occurrence of an enabled event type is recorded, where something other than the emission policy decides it: a SACL, a token's audit policy. Declared per event type in its fragment.

Asserted field. A field whose value, on a kernel-originated record, may have been supplied by userspace rather than observed by the kernel (§6.7).

Terms defined in PSPK §2 — event, header, payload, origin class, sequence — are used here with the same meaning and are not redefined, and nor are those of PCDS (SID, GUID, LUID, security descriptor, SACL, ACE), those of the registry (key, value, REG_DWORD, Machine\), which are the Peios Kernel TRM's, or package name, which is PSPU §5.3's. MessagePack is the format of the MessagePack specification, version 2.0.

6.2.1 Notation #

6.2.1.1 Field types #

A field's type is written storage.semantic: how the value is stored on the wire, then what it means. bin.sid is a binary SID, uint.time a point in time carried as an unsigned integer. One name carries both, so a reader knows the wire form and the rendering without a lookup. A type followed by [] is an array of that type. The types are listed in §6.5.

6.2.1.2 Pseudocode #

The pseudocode in §6.9 uses the corpus conventions of the Conventions book §2.3, and in addition:

NotationMeaning
and, or, notBoolean operators
for each x in listIteration in list order
absentNo value: a key or registry value that does not exist
returnEnds the procedure with the value given

6.3 Event Types

Peios / Peios technical reference / PGSS / Events

An event type reads left to right as who wrote it, what it is about, and what happened:

<root>.<noun>[.<noun>…].<verb>

kacs.audit.access.checked, peinit.job.started and org.jellyfin.server.playback.started are event types.

6.3.1 Form #

An event type MUST have at least three segments: a root, at least one noun, and a verb.

Every segment other than those of a package-name root (below) MUST match:

[a-z][a-z0-9]*(-[a-z0-9]+)*

That is, lowercase ASCII, kebab-case within a segment, and no underscores, capitals or empty segments. The event type MUST NOT contain /.

6.3.2 Roots #

The root names the owner of the event type.

A platform component has a platform root of one segment, listed in §6.A. A platform root names a component, not a package: several components shipped in one package each have their own root. An emitter MUST NOT use a platform root it is not listed against.

Every other emitter roots its event types at its own package name (PSPU §5.3), split into segments at its periods. A package named org.jellyfin.server writes org.jellyfin.server.playback.started. This holds for first-party applications as much as for third parties. An emitter MUST NOT root an event type at a package name that is not its own.

A platform root MUST NOT be a top-level domain in the DNS root zone, so that no platform root can be mistaken for the first segment of a package name. netd is a valid platform root; net is not.

Note

A package name may contain characters the segment grammar above does not admit — a leading digit, or +. Its segments are used as they are. A query naming such a type has to quote it, because the query language's unquoted identifiers start with a letter and exclude + (PSPU §3.19).

A consumer can derive an event type's owner from its name: a platform root through §6.A, and a package-name root as the longest installed package name its leading segments spell. A consumer MUST NOT treat the root as evidence of who wrote the event. The record header is that evidence (§6.4): nothing in this version prevents an emitter from writing under a root it does not own.

Note

Enforcing root ownership needs a way to prove which package a running program belongs to, which Peios does not yet have. It is left to a later attested-events mechanism rather than approximated now. Until then, review of a package submitted to the Peios package repository includes checking that its events use its own root.

6.3.3 Nouns #

After the root come one or more nouns, from general to specific.

An emitter SHOULD add a noun segment only where someone would plausibly write a policy on the set of event types beneath it. A middle segment that groups nothing anyone would grant, deny or switch off together costs every name a segment and buys nothing; tidiness alone is not a reason. The exception is a segment needed so that two event types do not collide.

Depth is preferred to compounds: lcs.audit.backup.started, not lcs.audit.backup-started. A compound word within a segment is for a single concept that has no useful parent, such as copied-up.

6.3.4 The verb #

The last segment is a past-tense verb saying what happened: created, started, ended, checked, used, refused.

The verb SHOULD be neutral where a neutral verb still says what happened, and MUST NOT be so neutral that it does not. installed is right for a package installation even when the installation failed, because the failure is carried in outcome.success (§6.6) and any consumer can show it. transaction.ended is wrong: it does not say what the transaction was.

An action that can succeed or fail is one event type (§6.6). An event type that is only ever written on failure MAY say so in its verb, as stratafs.mutation.refused does.

Distinct steps of a lifecycle are distinct event types: peinit.job.created, peinit.job.started and peinit.job.ended are three things that happen, not three outcomes of one.

6.3.5 Every dot is a policy anchor #

A consumer that controls read access per event type resolves an event's descriptor by trying the full type, then each shorter dotted prefix (PSPU §3.28). A descriptor on kacs.audit therefore covers the audit trail without the rest of KACS, and the emission policy is keyed the same way (§6.9). This is why the noun rule above is about policy, and why the segments are fixed rather than left to taste.

Note

A form with a slash, dev.peios.peinit/job.created, was considered and rejected. In log origins a slash separates a producer from a part of it below policy granularity, and the descriptor resolver stops at the first slash. Every event type would then resolve only at its package, and the slash is not an identifier character in the query language, so every query would need quotes.

6.4 Fields

Peios / Peios technical reference / PGSS / Events

A field is named for the part it plays in the event, then for what it is, then for which property of it the value is:

<role>.<thing>[.<thing>…].<attribute>      subject.token.sid
<domain>[.<…>].<attribute>                 outcome.success

6.4.1 Nesting is the name #

A payload is a MessagePack map with string keys. A field's path is the chain of keys leading to its value: subject.token.sid is the value at {subject: {token: {sid: …}}}. An emitter MUST encode a path as nested maps, one map per segment, and MUST NOT write a key containing .; such a key is a different and unreachable field (PSPU §3.22).

Every segment of a path MUST match the segment grammar of §6.3, including the segments of a package name that a third party's own fields sit beneath (below). A package whose name has a segment the grammar does not admit cannot define fields of its own.

A path is either a value or a map, never both. Once any event carries a value at a path, no field may be defined beneath that path, and a path defined as a map MUST NOT carry a value in any event. The shape of a subtree is therefore decided before its first field is defined, and the variant rule below exists so that it rarely has to be.

6.4.2 Roles #

A path that describes a participant leads with its role. The roles are a closed set.

RoleParticipant
subjectWhose authority the action used: the principal that acted
objectWhat the action was done to
sourceWhere something came from: the origin of a copy, of traffic, of data a backend supplied
destinationWhere it went
emitterWho was running when the record was written

After the role comes the thing, and then the attribute: subject.token.sid, object.file.path, source.stratum.index.

The emitter and the subject are different participants. The emitter is whoever wrote the record; the subject is whoever acted. They are the same on most events, and differ on every event one component writes about another: an access check a daemon requests for a client has the daemon as emitter and the client as subject. A consumer searching for what a principal did MUST search subject.*, not emitter.*.

An event about an action MUST carry subject.token.sid, the SID of the principal that acted, whether that principal is the caller whose token the action ran under or the emitter acting on its own authority. The header carries only token GUIDs (below), never a SID, so the SID is a payload field in every case.

6.4.3 Domains #

A path that does not describe a participant leads with a domain, then the attribute: outcome.success, access.granted.

A domain names either a property of the event itself, or a thing the event happened within without acting on it: a ring buffer, a connection-tracking flow, a transaction. A thing the event acts on is a participant and takes a role (object.mount.path); a thing it happens within leads with its own name (buffer.fill, flow.state).

The generic roots are shared by every emitter and defined in the platform fragment (§6.10):

RootHolds
subject, object, source, destination, emitterThe participants, and their common things: token, process, file, key, session
eventThe record's own header values (below)
accessRights asked for, granted and audited
outcomeWhether the action succeeded, and why not (§6.6)
triggerWhatever caused the record to exist: an audit ACE, a blocking layer, a causing file
operationWhat was attempted and how
configA configuration value reported on
policyA rule set in force
transactionA transaction an operation belonged to
fieldsProperties of the record's fields (§6.7)

A platform component MAY define a domain of its own for things only it has, such as a ring buffer or a service graph, and owns it (§6.10). Any other emitter's own fields go beneath its package name (below), because a domain is one segment wide and two vendors' choices would collide.

6.4.4 The header #

The record header (PSPK §2) is stamped by the kernel and cannot be forged by the emitter. A consumer that exposes header values by name MUST expose them under these field paths:

Header field (PSPK §2)Field pathType
typeevent.typestr
timestampevent.timeuint.time
sequenceevent.sequenceuint
cpu_idevent.cpuuint
—event.boot.guidbin.guid
origin_classemitter.classuint.enum
effective_token_guidemitter.token.guidbin.guid
true_token_guidemitter.true-token.guidbin.guid
process_guidemitter.process.guidbin.guid

event.boot.guid is not in the record: it is the boot the consumer read the record in, which the consumer supplies. emitter.class is a uint.enum whose numbers are PSPK §2's origin classes, which is why it is not a string (§6.5).

An emitter MUST NOT write any of these paths in a payload. Details of the emitting process that the header does not carry — its PID, name and executable — are payload fields under emitter.process.

6.4.5 Things and their relations #

A thing related to a participant nests under it: the parent of a process is object.process.parent.pid, the target of a registry symlink is object.key.target.guid, the token a job runs as is object.job.token.sid.

Two things of one kind that appear in one event are told apart by direction where there is one: source and destination. Where there is no direction, the second is named by its relation to the first, as above. A path MUST NOT use other, peer or a similar segment to tell two participants apart.

key as a thing means a registry key and nothing else. A cryptographic key is a signing-key, a handle is a handle.

capability as a thing or attribute means a confinement capability SID. A Linux capability is linux.cap, which is context for a privilege decision and never the decision itself.

6.4.6 Many values #

Many values of one field go in an array under a plural name: subject.token.groups. Arrays that describe the same items are parallel and share an index: subject.token.group-attributes[i] describes subject.token.groups[i].

6.4.7 Variants #

A variant of a value is a sibling of it, named with a qualifier suffix, and the plain name always holds the value in force:

PathValue
policy.generationThe generation now in force
policy.generation-previousThe generation it replaced
object.key.pathThe key's resolved path
object.key.path-requestedThe path as the caller wrote it

A qualifier MAY also sit on a thing, where the variant is a whole thing: source.stratum-previous.path.

The following qualifiers MAY be applied to any defined field or thing without that variant being defined separately: -previous, -requested, -expected, -effective, -limit and -count. This set is closed in this version. Any other qualifier is part of a field's name, and the field MUST be defined like any other (§6.10).

Note

Variants are siblings rather than children because of the value-or-map rule. If before and after were last segments, policy.generation.previous could not exist beside a policy.generation that other events already carry as a value. A sibling also sorts next to the value it qualifies.

6.4.8 Standard attributes #

These attributes MAY be attached to any defined thing or domain without being defined separately: count, limit, length, size, depth, capacity, fill, duration, timeout, attempts, offset, index, sequence, generation, digest, name, path, id, guid, sid, type and state. Each has the type its name implies in §6.5. This set is closed in this version.

6.4.9 Who may define a path #

Every path is defined exactly once across the catalogue (§6.10).

The generic roots of the table above, and their common fields, are defined in the platform fragment. A component defines the subtrees of things only it knows about — StrataFS defines source.stratum.* — and any domain of its own. Any emitter MAY carry a field defined by another fragment without defining it.

A field that no generic path or platform subtree covers, and that an emitter that is not a platform component needs, sits beneath its own package name below the role: object.org.jellyfin.server.media.title. Such a field SHOULD be rare, and an emitter SHOULD use a defined field wherever one has the meaning it needs.

An emitter MUST NOT write a field that the catalogue does not define, other than a variant or standard attribute permitted above.

6.4.10 Names are access control #

A consumer that controls read access per field treats each path as an object in an access check (PSPU §3.28). Because the path is nested, a grant on subject can cover every identity field beneath it, including fields defined later. Because each value has one path, a descriptor that denies a value cannot be evaded by writing the value under a second name. That is the security reason for the one-definition rule, and why the catalogue rejects a duplicate (§6.10).

6.5 Values

Peios / Peios technical reference / PGSS / Events

Every value has one wire form, and the field's type names it. A payload is one MessagePack map with string keys (PSPK §2).

6.5.1 Types #

ValueWire formType
PrincipalA binary SID (PCDS)bin.sid
GUIDThe 16-byte binary GUID (PCDS)bin.guid
LUIDUnsigned integeruint.luid
ACEThe binary ACE (PCDS)bin.ace
Point in timeUnsigned integer, nanoseconds since the Unix epochuint.time
DurationUnsigned integer, nanosecondsuint.duration
Size in bytesUnsigned integeruint.bytes
Access maskUnsigned integeruint.mask
FlagsUnsigned integeruint.flags
Integrity levelUnsigned integer, the integrity RIDuint.integrity
Error numberSigned integer, negativeint.errno
BooleanMessagePack booleanbool
EnumerationString, kebab-casestr.enum
Numeric enumerationUnsigned integeruint.enum
PathStringstr.path
IP addressString, in the textual form for its familystr.ip
DigestBinarybin
TextStringstr
Other integerUnsigned or signed integeruint, int

An emitter MUST encode a value in the wire form of its field's type. A consumer MUST NOT accept a different form as the same value: a SID written as an SDDL string is not a bin.sid.

Enumerations are symbolic. An enumeration is a kebab-case string naming the value, wrong-type rather than 2. A uint.enum is permitted only where the numbers are fixed by another specification or an external standard — an impersonation level, an address family — and a consumer renders it by name. Every enumeration declares its values in its fragment, or names the reason-code family it draws them from, and states whether the set is closed (§6.10). A consumer that meets a value of an open enumeration it does not know MUST treat it as a value rather than an error, and SHOULD show it as written.

Masks need their kind. A uint.mask is decoded against the table for the kind of thing it applies to, which the same record names in object.kind. An event that carries a uint.mask field MUST carry object.kind.

6.5.2 What an emitter does not write #

  • An absent value. A field with no value is omitted: the key is not written. An emitter MUST NOT write nil, an empty string or zero to mean "none".
  • The header again. An emitter MUST NOT repeat in its payload a value the header already carries: the time, the sequence, or the emitter's token and process GUIDs (§6.4).
  • A name beside a SID. A principal is carried as its SID only. An emitter MUST NOT carry a principal's name in a field; resolving a SID to a name is a consumer's convenience, done when the record is read.
  • Units in a name. The type carries the unit. A duration is operation.duration, never operation.duration-ns.
  • Debugging output. A string value MUST NOT be the output of a language's debug formatter. Its format is not stable and no consumer can rely on it.

Note

Names are kept out of events because a name is not stable and a SID is. A principal renamed after the event would otherwise appear under its old name in every record written before the change, and a search by the new name would miss them.

6.5.3 Departures from the shared conventions #

This chapter departs from the data conventions of the Conventions book §3.3 on two points:

  • Timestamps are unsigned integers counting nanoseconds since the Unix epoch, not RFC 3339 strings. The record header already carries time this way (PSPK §2), and one form for every time in a record lets a consumer compare them directly.
  • Digests are carried as binary, not lowercase hexadecimal. A consumer renders them as hexadecimal for display.

6.6 Outcomes

Peios / Peios technical reference / PGSS / Events

An action that can succeed or fail is one event type, and its result is carried in outcome.*. "Everything that failed" is then one query across every emitter, and no descriptor can show an action's successes while hiding its failures.

An emitter MUST NOT define separate event types for the success and the failure of one action. A type that only ever records failure is permitted (§6.3).

6.6.1 The outcome fields #

FieldTypeCarries
outcome.successboolWhether the action did what was asked
outcome.reasonstr.enumWhy it failed, or why a success was unusual
outcome.errnoint.errnoThe error number it failed with
outcome.detailstrText for a person to read

An event whose action can fail MUST carry outcome.success.

outcome.reason is symbolic. Its values are drawn from a named reason-code family, or from an enumeration the event declares in its fragment (§6.10). Where a kernel reason code exists for the decision, the value is that code's name in kebab-case with its family prefix removed, so that an event and a trace of the same decision describe it in the same terms.

outcome.errno is the negative error number the operation returned, -13 for EACCES. It is omitted on success. A consumer renders it by name.

outcome.detail is optional free text for a person. Nothing parses it, and an emitter MUST NOT carry anything in it that a consumer would need to filter on: that belongs in outcome.reason or in a field of its own.

Where a decision has more than two results, so that a boolean cannot express it, outcome.verdict carries the result as an enumeration.

Note

Before this chapter, the reason for one failure was written as failure_reason, failure_cause, reason, error, detail, parse_error, finalization_detail, result and message, and a descriptor denying one of them denied none of the others. The four fields above replace all nine.

6.7 Access Decisions

Peios / Peios technical reference / PGSS / Events

Every access decision is recorded the same way: by the kernel's access audit, under the SACL of the object decided on. That holds for a decision a userspace component makes about objects it guards itself — a service manager's services, an event store's namespaces — as much as for a file. "Who was refused what" is then one query, and a SACL governs a daemon's objects as it governs a file.

6.7.1 A component that guards objects #

An emitter that decides access to objects of its own:

  • MUST make each decision through the kernel's access check, against the object's security descriptor;
  • MUST pass the object's identity as the check's audit context (below);
  • MUST NOT write an event of its own recording the decision, whether it granted or refused. It MAY write a log line about a refusal for its own debugging; that line is not the audit record.

6.7.2 The audit context #

The audit context is one MessagePack map:

KeyValue
kindThe kind of object, a kebab-case string: service, job
<kind>Optional. A map of the object's identifying fields, under a key equal to the value of kind
{kind: "service", service: {name: "jellyfin"}}

The map MUST contain kind, MAY contain the map named by it, and MUST contain nothing else. The kernel rejects a context that is not exactly this shape: kind must be a string matching the segment grammar of §6.3, the map under the kind's key must not be empty, and every key in that map must match the same grammar, because each becomes a segment of object.<kind>.<key>. A context with no body map is accepted; an empty body map is not. Each field in that map MUST be a field the component's fragment defines beneath object.<kind>, and MUST be marked asserted there (§6.10).

The kernel's record of the decision then carries object.kind and the object's fields as object.service.name and so on, beside the subject, the access requested and granted, and the trigger, exactly as for any other object. How the kernel's access-check interface accepts and copies the context is described in the Peios Kernel TRM.

6.7.3 Asserted fields #

The kernel cannot verify that a descriptor belongs to the service a component names. A field the kernel copies from userspace is therefore the component's claim: it is the identity of what the component actually enforced, but the kernel did not observe it.

A kernel-originated record that carries any asserted value MUST carry fields.attestation.userspace with the value true. A record that carries none MUST omit the field. A record written by userspace never carries it, because the origin class in its header (§6.4) already says that the whole payload is the emitter's word.

A consumer MUST NOT present an asserted field as observed by the kernel.

6.7.4 The emitter and the subject #

On a decision a component requests for a client, the record's header names the component, as emitter.*, and the payload's subject.* names the client whose access was checked (§6.4). The component's process details are emitter.process.*.

6.7.5 Default SACLs #

A component that guards objects SHOULD give each a default SACL with an audit ACE that records failed access by Everyone, so that a refusal is recorded without configuration. An administrator can narrow or remove it per object like any other SACL. The exception is a kind of object so numerous that recording every refusal would bury the record, which is why files and registry keys carry no default SACL.

A component that stores a descriptor where writing the stored value replaces the SACL without the privilege that normally guards a SACL — for example, a whole descriptor kept as one registry value — MUST allow that store to be written only by principals trusted to change audit policy. A tool that edits such a descriptor's DACL MUST keep its SACL intact.

Note

Two other ways of recording refusals were considered. Adding a SACL to every descriptor just before the check would make the record's trigger.ace name an ACE that is not in the object's descriptor, could not be changed per object, and would be done differently by every component. Turning on failure auditing in every token's audit policy would record every refusal on every object, files included; that remains the tool for auditing everything one principal does.

6.8 Events, Logs and Tiers

Peios / Peios technical reference / PGSS / Events

6.8.1 Event, log or metric #

An event is a discrete occurrence that someone may search for, alert on, or attribute to a principal, one record at a time. It carries an identity the kernel stamped (PSPK §2).

A log line is narrative for the people who maintain a component. Its value is in reading the sequence, not in querying single records.

A metric is a sampled quantity, where the trend matters and no single sample does.

An emitter MUST decide between them by two tests, in order:

  1. Does an operator care? Events and metrics are for operators; logs are for developers. If an operator would not act on it, it is a log line, and the second test does not apply.
  2. Does one instance matter on its own? If it does, it is an event, however frequent it is. If only the trend matters, it is a metric.

An emitter MUST NOT carry log text in an event. A message a person reads belongs in outcome.detail (§6.6) where it explains the outcome, and in the log otherwise.

Note

An operator should not need to read logs in all but the rarest cases. If they do, something that should have been an event was written as a log line.

6.8.2 Tiers #

Every event type declares one tier in its fragment (§6.10). The tiers are a closed set.

TierMeaningOn unless the policy says otherwise
essentialExtremely important and relatively rare, or governed by its own configurationAlways; cannot be switched off
standardWhat an administrator reviews: lifecycle, configuration, failuresYes
verboseDetail for an active investigationNo
debugFor the component's developersNo

A tier sets only the default. The emission policy switches a type on or off whatever its tier, except an essential type, which it never switches off (§6.9). A shipped default policy MUST NOT switch a debug type on.

6.8.3 Essential #

An event type is essential for one of two reasons:

  • It is extremely important and relatively rare. A logon session ending, or a configuration value rejected.
  • Its emission is already governed by its own configuration. The kernel's access audit is the example: a SACL, or a token's audit policy, decides which occurrences are recorded. So is any event whose own configuration, such as a REPORT directive, decides whether it is written.

The second reason follows a principle that holds across Peios: one switch per thing. Something already governed by its own configuration does not also need an emission-policy switch before it works. Making an administrator turn on the same thing in two places is what this rules out.

An emitter MUST NOT declare an event type essential for any other reason.

Note

Nothing in a running system stops a third party declaring an event essential, and nothing tries to. Whether an essential event meets one of the two reasons is one of the checks made when a package is submitted to the Peios package repository.

6.9 Emission Policy

Peios / Peios technical reference / PGSS / Events

An event type that is switched off is never written. Each emitter decides, before it writes an event, whether its type is on, from a policy kept in the registry. A consumer decides what to keep of what it reads; it does not decide what is written.

The component that writes a record decides, whatever path the record takes. A component that writes records of its own without going through the kernel's event stream, straight into a store for example, is their emitter for the purposes of this section, and applies the policy to every type it writes that is not essential.

6.9.1 The policy tree #

The policy is the registry key Machine\Generic\Events and the keys beneath it. There is one key per segment of an event type, so the policy for an event type is found on the path that spells its segments beneath Events, one key each, in order: the policy for kacs.audit.access.checked is found by walking Events\kacs\audit\access\checked.

A package-name root (§6.3) is split at its periods like the rest of the type, so a package's events sit beneath one key per segment of its name: org.jellyfin.server.playback.started is found by walking Events\org\jellyfin\server\playback\started, and Events\org\jellyfin is the key that covers every package named beneath org.jellyfin. A segment is used as it is, including one a package name gives that the segment grammar of §6.3 does not admit.

The registry compares key and value names ignoring case, so the walk does too.

Any key on such a path MAY hold a value named Enabled. A REG_DWORD of 1 switches the event types beneath it on, and 0 switches them off. An Enabled value of any other type, or a REG_DWORD holding any other number, is ignored, as though it were absent.

Nothing else in the tree has a meaning. A key on no event type's path, and any value other than Enabled, decides nothing.

6.9.2 Resolving a type #

Whether an event type is on is decided by this procedure. The deepest Enabled value on the type's path wins, and the type's tier decides when there is none.

enabled(event_type, tier) → bool
    if tier == essential
        return true                       // never consults the policy
    setting = absent
    key = Machine\Generic\Events          // absent if it does not exist
    for each segment in segments(event_type)
        if key == absent
            break
        setting = read_enabled(key, setting)
        key = subkey(key, segment)        // absent if it does not exist
    if key != absent
        setting = read_enabled(key, setting)
    if setting != absent
        return setting == 1
    return tier == standard               // standard on; verbose and debug
                                          // off; essential returned above

read_enabled(key, current) → setting
    value = value(key, "Enabled")         // absent if it does not exist
    if value is REG_DWORD and (value == 0 or value == 1)
        return value
    return current                        // ignored: keep what was found

Each rule of the procedure holds on its own:

  • The Enabled value on the deepest key that has one decides. A value on Events itself applies to every type the keys beneath it do not decide.
  • The key named by the whole type, its last segment included, is read like the keys above it.
  • Where a key on the path does not exist, the keys beneath it do not either, and the deepest value found above it decides.
  • A key the emitter may not read ends the walk as a missing one does.
  • With no Enabled value on the path, a standard type is on, and a verbose or debug type is off (§6.8).

An emitter MUST NOT write an event whose type this procedure finds off.

An emitter MUST write an essential event without consulting the policy, whatever the tree holds.

An emitter SHOULD make the decision before it builds the event's payload, so that a type that is off costs nothing beyond the decision.

6.9.2.1 Example #

With no value on Events, Events\kacs holding Enabled = 0, and Events\kacs\audit holding Enabled = 1:

Event typeTierDecided byOn
lcs.config.value.rejectedstandardits tierYes
kacs.caap.staging.divergedstandardEvents\kacsNo
kacs.audit.handle.usedstandardEvents\kacs\auditYes
kacs.audit.access.checkedessentialits tier, alwaysYes

With Enabled = 0 on Events as well, lcs.config.value.rejected is off and the other three are as before. A verbose or debug type in the place of lcs.config.value.rejected would be off in both trees, and in the place of kacs.audit.handle.used on in both.

6.9.3 Gating, SACLs and audit policy #

For an event type with gating (§6.2), the policy and the gating decide together, by AND: the type is written for an occurrence only if the policy has it on and its gating asks for that occurrence.

Neither widens the other. Switching a type on does not record an occurrence that a SACL, or a token's audit policy, does not ask for; and a SACL that asks for an occurrence does not record it while the policy has its type off. The policy decides whether a kind of event exists at all. The gating decides which occurrences of it are recorded, and only the emitter can evaluate it, because only the emitter can see the object.

An essential type's gating alone decides, because the policy never switches it off (§6.8). The kernel's access audit is the case this is for: kacs.audit.access.checked is essential, so whether an access decision is recorded is the SACL's and the audit policy's choice alone, and an administrator never has to switch the same thing on in two places.

6.9.4 Noticing a change #

An emitter MAY cache the policy in-process.

An emitter MUST apply a change to the policy to every decision it makes one second or more after the change was committed, whether or not it caches. An emitter SHOULD apply it to the first decision it makes after the commit, which an emitter that watches the tree can do.

A watch on the subtree of Events sees every change. An emitter that writes under few roots MAY instead watch only the keys on its own types' paths. A key that does not exist cannot be watched, so an emitter watching for one to be created watches the nearest key above it that does exist.

A change is not retroactive. An event written before it stays written, and an occurrence decided before it stays decided.

6.9.5 When the policy cannot be read #

An emitter that cannot read Machine\Generic\Events — before the registry has a source for the Machine hive, where the key does not exist, or where it may not read it — MUST decide by tier alone, as the procedure does when no Enabled value is found.

It MUST NOT hold an event back to wait for the policy to become readable.

Once the policy can be read, an emitter MUST apply it to every decision it makes one second or more after it became readable.

Note

The kernel writes events from early in boot, before any registry source has registered. For that window its decisions are tier defaults: a verbose type the policy switches on is not written, and a standard type the policy switches off is. An operator who needs a boot-time event whatever the policy says has the essential tier, if the event meets one of its two reasons (§6.8), and nothing else.

6.9.6 Who may change it #

Each key's security descriptor decides who may change the policy beneath it, and inheritance carries that down the tree. This is why the policy has one key per segment rather than one flat key per pattern: a grant on Events\org\jellyfin lets whoever runs Jellyfin switch its events, and reaches nothing else.

A conforming system:

  • lets every emitter read Machine\Generic\Events and every key beneath it;
  • lets only administrators write Machine\Generic\Events itself, and the keys of platform roots (§6.A);
  • SHOULD give Machine\Generic\Events a SACL that audits writes, so that a change to what is recorded is itself recorded.

Note

Leaving the decision to the consumer was considered and rejected. The kernel's event stream drops its oldest events when a ring is full, whatever they are, so events nobody wanted would push the audit trail out of the ring. They would also cost an encoding on hot paths, and every reader of the ring would still see them.

6.10 The Catalogue

Peios / Peios technical reference / PGSS / Events

The catalogue is the single source of every event type and field definition. Everything else that lists them — reference documentation, a viewer's field picker, a policy editor — is generated from it or checked against it.

6.10.1 Fragments #

An emitter MUST describe every event type it writes, and every field it defines, in a fragment: a UTF-8 text file installed in /usr/share/evman/ with the extension .evman. A package installs one fragment per component it ships, named for the component.

The platform fragment, /usr/share/evman/kernel.evman, defines the generic roots of §6.4 and their common fields. A conforming system has it.

A consumer that reads the catalogue reads every .evman file in /usr/share/evman/, and no subdirectory.

6.10.2 Format #

A fragment is a sequence of records. Each record begins with an anchor line of three hyphens, a space, the record kind, a space and the name it defines:

--- field subject.token.sid
--- group subject
--- event kacs.audit.access.checked

After the anchor come header lines of the form key: value, one per line, then a blank line, then prose: the record's description. The prose's first sentence is a complete summary of it. Inline markup in prose is limited to **bold** and `code`.

6.10.2.1 Field records #

KeyRequiredValue
typeYesThe field's type (§6.5)
valuesFor an enumerationThe values, separated by |, or the name of the reason-code family they are drawn from
closedFor an enumerationtrue if no value may be added without a new version of the field
assertedNotrue if the value may be supplied by userspace on a kernel-originated record (§6.7)
carriedNoheader for a header field (§6.4); absent for a payload field

6.10.2.2 Group records #

A group names a set of fields that always travel together, so that events can include the set rather than list it. Its header lines are include: lines, one field path each.

6.10.2.3 Event records #

KeyRequiredValue
tierYesThe event type's tier (§6.8)
gatingIf anyWhat decides which occurrences are recorded
cardinalityNoHow many records one occurrence produces, where that is not one

After the prose come the fields the event carries, one per line:

include: subject
field: object.kind              required
field: object.file.path         when object.kind == file
field: access.granted           required
  The mask this check granted.

include: names a group. field: names a field, followed by when it is present: required, optional, or when and a condition on another field of the record. An indented paragraph after a field: line is a gloss: what the field means in this event. A gloss MAY narrow the field's definition and MUST NOT contradict it.

Where a field's definition leaves its values to the event, as outcome.reason does (§6.6), the event declares them with indented values: and closed: lines directly after the field: line, in the form a field record uses:

field: outcome.reason           when outcome.success == false
  values: disk-full | source-unreadable | cancelled
  closed: false
  Why the snapshot was not written.

6.10.3 Rules #

Every fragment MUST pass these rules, checked across the whole catalogue:

  1. Every path a field: or include: line names is defined.
  2. No path is defined by two fragments, and no path is defined twice in one.
  3. No path is defined both as a value and as a map (§6.4).
  4. Every event type a fragment defines begins with a root that fragment owns: a platform root listed against its component in §6.A, or its package's name.
  5. A platform component's fragment defines fields only in the subtrees and domains of things it alone knows about (§6.4). Any other fragment defines fields only beneath its own package name below a role (§6.4).
  6. Every name matches the grammar of §6.3 and §6.4, and every event type's last segment is a verb in the past tense.
  7. Every enumeration declares its values or names its reason-code family, and says whether it is closed.
  8. Every event type declares a tier, and an event type with gating declares it.
  9. An event type that carries a uint.mask field carries object.kind (§6.5).

A tool that reads the catalogue SHOULD report every rule a fragment breaks. A consumer that acts on the catalogue — one that decides access, builds a field tree for a descriptor, or validates payloads against it — MUST NOT treat a fragment that breaks rule 1, 2 or 3 as defining anything. A tool that only displays the catalogue MAY show such a fragment's records, provided it warns that the fragment breaks those rules.

Note

Rule 2 is what stops one value acquiring three spellings again. Before the catalogue existed nothing could have caught it, and the result was a field that a descriptor could deny under one name and leak under another (§6.4).

6.11 Extension

Peios / Peios technical reference / PGSS / Events

6.11.1 Additive changes #

The following are additive. An emitter and a consumer built to this version behave correctly in their presence:

  • A new event type, under a root its emitter owns.
  • A new field, defined once in the catalogue. A consumer that does not know a field still reads, stores and queries it by its path.
  • A new value of an open enumeration, which a consumer built to this version treats as a value rather than an error (§6.5).
  • A new platform root in §6.A, provided it is not already in use as a package name's first segment and is not a top-level domain.
  • A new domain of a platform component's own (§6.4).
  • A new key in a fragment record. A consumer that does not know a key ignores it.

6.11.2 Changes requiring a new version #

  • Renaming an event type or a field, or moving a field to another path. Every descriptor and every query written against the old name stops matching.
  • Changing a field's type or wire form (§6.5).
  • A new value of a closed enumeration.
  • Defining a field beneath a path that is a value, or giving a value to a path that is a map (§6.4).
  • A change to the roles, the qualifiers or the standard attributes of §6.4, or to the tiers of §6.8.
  • A change to the policy tree's location or its resolution (§6.9).

6.11.3 Reserved #

The roots in §6.A are reserved to the components listed against them. The roles and generic roots of §6.4 are this chapter's, and only the platform fragment defines fields directly beneath them. Every other fragment is bound by rule 5 of §6.10.

/usr/share/evman/ is this chapter's, and so is the .evman extension within it. Machine\Generic\Events and every key and value beneath it are this chapter's: nothing else may keep anything there.

6.12 Conformance

Peios / Peios technical reference / PGSS / Events

6.12.1 A conforming emitter #

  • Writes event types of at least three segments, kebab-case, ending in a past-tense verb, under a platform root listed against it or under its own package name (§6.3).
  • Writes one event type per action, with the result in outcome.*, and carries outcome.success wherever the action can fail (§6.6).
  • Encodes every path as nested maps, never as a key containing ., and writes only defined fields and the variants and standard attributes §6.4 permits (§6.4, §6.10).
  • Writes nothing at a header path, and does not repeat a header value in its payload (§6.4, §6.5).
  • Encodes each value in its type's wire form, omits a field that has no value, carries principals as SIDs without names, and writes no debugging output (§6.5).
  • Carries object.kind wherever it carries an access mask (§6.5).
  • Makes access decisions about objects it guards through the kernel's access check, passes the object's identity as the audit context, and writes no access-decision event of its own (§6.7).
  • Keeps a stored descriptor's SACL intact and its store writable only by those trusted with audit policy (§6.7).
  • Writes events, not log text, and declares each type's tier, choosing essential only for one of the two reasons of §6.8.
  • Writes no event whose type the emission policy finds off, and an event of a gated type only where its gating also asks for it; writes essential events without consulting the policy; applies a change to it within one second; and falls back to tier defaults, without holding events back, when it cannot read it (§6.9).
  • Installs a fragment describing every event type it writes and every field it defines, which passes the rules of §6.10.

6.12.2 A conforming consumer #

  • Exposes header values under the paths of §6.4.
  • Does not treat an event type's root as evidence of who wrote it (§6.3), and does not present an asserted field as observed by the kernel (§6.7).
  • Does not accept a value in a wire form other than its type's (§6.5).
  • Treats an unknown value of an open enumeration as a value, not an error (§6.5).
  • Reads the catalogue from every .evman file in /usr/share/evman/ and no subdirectory, and treats a fragment breaking rule 1, 2 or 3 as defining nothing (§6.10).

6.12.3 A conforming system #

  • Has the platform fragment at /usr/share/evman/kernel.evman (§6.10).
  • Lets every emitter read Machine\Generic\Events, lets only administrators write it and the keys of platform roots, and SHOULD audit writes to it (§6.9).

6.12.4 What conformance does not require #

Conformance does not require an emitter to write any particular event, to cache the emission policy, or to define fields beyond those it writes. It does not require a consumer to store, index or query events, to read the catalogue at all, or to resolve SIDs to names. It does not require either role to know about any event type or field other than the ones it writes or reads: an unknown event type is still an event, and an unknown field is still a value at a path.

Appendix 6.A Platform Roots

Peios / Peios technical reference / PGSS / Events

The platform roots (§6.3). Every other emitter roots its event types at its package name. This list is open: a root is added by appending it under §6.11, and is never reassigned.

RootComponentPackage
kacsKernel Access Control Subsystemdev.peios.kernel
lcsThe kernel's registry (configuration store)dev.peios.kernel
kmesKernel Mediated Event Subsystemdev.peios.kernel
stratafsStrataFSdev.peios.kernel
ntfeThe kernel's network-policy enginedev.peios.kernel
peinitThe service managerdev.peios.peinit
peipkgThe package managerdev.peios.peipkg
eventdThe event storedev.peios.eventd
authdThe authentication daemondev.peios.authd
lpsdThe local principal sourcedev.peios.authd-lpsd
timedThe time daemondev.peios.timed
netdThe network daemondev.peios.netd
trustdThe trust daemondev.peios.trustd

These roots are reserved for components that write no events yet, and MUST NOT be used by anything else:

RootComponent
resolvdThe name-resolution daemon
udThe user daemon
loregdThe log store

Note

A root names a component rather than a package because one package ships several components with separate concerns: the kernel package ships five, and an administrator who wants the access audit without the network-policy reports needs kacs and ntfe to be separate policy anchors.

Appendix 6.B Prior Art

Peios / Peios technical reference / PGSS / Events

This appendix is informative.

6.B.1 Windows event logging and auditing #

Peios's access model is Windows's, and so is the shape of its audit: SACLs with audit ACEs decide which accesses are recorded, and a token's audit policy can force recording for one principal. Windows audit policy also sits above SACLs, so that a category switched off suppresses even records a SACL asks for. §6.9 keeps that relation for a gated event type that is not essential, and drops it for the access audit itself, which is essential so that a SACL alone decides (§6.8).

This chapter departs from Windows in its names. Windows identifies events by numeric ID within a provider, and its payload fields are named per event; the same value is SubjectUserSid in one event and TargetSid in another. Here the name is the policy anchor, so every event type is a dotted name and every value has one path.

6.B.2 Elastic Common Schema #

The Elastic Common Schema names fields by dotted, nested paths and shares a field set across sources, so that one query finds a host or a user wherever it appears. This chapter takes the same approach.

It departs in leading with the participant's role rather than its kind: subject.token.sid, where the schema leads with the kind of thing. A consumer searching for a principal in any role searches across roles either way, and leading with the role makes subject a single policy anchor for "may see who did it".

6.B.3 OpenTelemetry semantic conventions #

OpenTelemetry names attributes by dotted paths and keeps a registry of them with stability levels, so that a name is defined once and referenced everywhere. The catalogue of §6.10 plays the registry's part, and its rule that no path is defined twice is the same discipline, checked mechanically.

It departs in fixing the type and wire form of every value, rather than leaving encoding to each exporter, because the event stream is consumed directly and a SID or a time written two ways is two values to every query.

Peios Learn — documentation for the Peios project.

Built with Trail.