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.
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.
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.
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.
- The client sends exactly one opening message:
LogonStart,CredentialChangeStartto change its own credential (§2.20),CredentialEnrollStartto add or remove one of its own (§2.23), orSessionEndorSessionEndQueryabout a logon session (§2.22). It MUST be the first message. An authority MUST reject any conversation that opens with something else. - The authority sends zero or more
CredentialRequestmessages. Each MUST be answered by exactly oneCredentialResponsebefore the authority sends anything further. - The authority sends exactly one terminal message:
AccessGrantedorAccessDeniedfor a logon,CredentialChangedorAccessDeniedfor a credential change or an enrolment,SessionEndedorAccessDeniedfor aSessionEnd, andSessionEndAllowedorAccessDeniedfor aSessionEndQuery. - 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.
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.
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:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | magic | PGSL (50 47 53 4c) |
| 4 | 2 | version | 1 |
| 6 | 2 | msg_type | See §2.A |
| 8 | 4 | total_len | Header 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
u32byte count, and the bytes follow immediately. - Structures and array elements are prefixed with a
u32byte 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 #
| Field | Encoding | Limit |
|---|---|---|
logon_type | u8 | §2.B |
identifier_type | u8 | §2.B |
identifier | length-framed bytes | 1024 |
tty | string | 128 |
remote_host | string | 256 |
supported_credential_types | length-framed bytes, one u8 per type | 32 |
required_credential_type | u8; 0 none, 1 Password, 2 SshPublicKey | §2.D |
has_ssh_binding | u8, exactly 0 or 1 | |
ssh_binding | structure, 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.
| Field | Encoding | Limit |
|---|---|---|
messages | array of Message | 8 |
prompts | array of Prompt | 16 |
Message:
| Field | Encoding | Limit |
|---|---|---|
severity | u8 | §2.B |
text | string | 512 |
Prompt:
| Field | Encoding | Limit |
|---|---|---|
credential_ref | u32 | — |
credential_type | u8 | §2.B |
credential_name | string | 128 |
parameters | length-framed bytes | 4096 |
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.
| Field | Encoding | Limit |
|---|---|---|
answers | array of Answer | 16 |
Answer:
| Field | Encoding | Limit |
|---|---|---|
credential_ref | u32 | — |
data | length-framed bytes | 32768 |
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.
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.
| Field | Encoding |
|---|---|
session_id | u64 |
profile | length-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.
| Field | Encoding | Limit |
|---|---|---|
home | string | 4096 |
shell | string | 4096 |
display_name | string | 256 |
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.
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.
| Field | Encoding | Limit |
|---|---|---|
denial | u32 | §2.B |
reason | string | 512 |
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.
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.
| Value | Name | Collection |
|---|---|---|
| 1 | Password | A line of text, not echoed |
| 2 | SshPublicKey | Bound 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.
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.
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).
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).
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.
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.
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:
| Character | Reserved 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.
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.
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.
| Field | Encoding | Limit |
|---|---|---|
tag | u32 | §2.14 |
key_type | u8 | §2.B |
name | string | 256 bytes |
sid | length-framed bytes (SID) | 68 bytes |
unix_id | u32 | |
kind | u8 | §2.B |
fields | u32 | §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 #
| Value | Name | Answers |
|---|---|---|
| 1 | Name | getpwnam, getgrnam |
| 2 | Sid | Rendering a security descriptor |
| 3 | UnixId | getpwuid, 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 #
| Value | Name |
|---|---|
| 0 | Any |
| 1 | Principal |
| 2 | Group |
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.
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.
| Bit | Name | Value encoding |
|---|---|---|
| 0 | UNIX_ID | u32 |
| 1 | PRIMARY_GROUP | reference |
| 2 | HOME | string, 4096 bytes |
| 3 | SHELL | string, 4096 bytes |
| 4 | DISPLAY_NAME | string, 256 bytes |
| 5 | GROUPS | array of references, 128 |
| 6 | MEMBERS | array of references, 256 |
| 7 | CLAIMS | array of claim entries, 64 |
| 8 | ENABLED | u8 |
| 9 | LOGON_TYPES | u32 |
| 10 | DESCRIPTION | string, 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.
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.
| Field | Encoding | Limit |
|---|---|---|
tag | u32 | §2.14 |
outcome | u8 | §2.B |
sid | length-framed bytes (SID) | 68 bytes |
qualified_name | string | 512 bytes |
kind_found | u8 | §2.B |
present | u32 | §2.B |
withheld | array of withheld entries | 32 |
values | array of length-framed values | 32 |
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 invalues; - 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:
| Field | Encoding | Limit |
|---|---|---|
field | u32 | one bit, §2.B |
reason | u8 | §2.B |
| Reason | Meaning |
|---|---|
Absent | The field has no value. |
Restricted | The caller may not have this field. |
Declined | The source will not produce it. |
TooLarge | It 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:
| Field | Encoding | Limit |
|---|---|---|
sid | length-framed bytes (SID) | 68 bytes |
name | string | 512 bytes |
unix_id | u32 |
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:
Declinedwhere the source will not enumerate the group's members.TooLargewhere the membership exceeds what one reply can carry.Absentwhere 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,
MEMBERSis withheld asTooLarge, and the members are walked withEnumerate(§2.17). - Where a source in the order could not be reached, or would not
enumerate,
MEMBERSis withheld asDeclined. A list missing one source's members would look complete.
2.16.4 One round trip #
An authority MUST be able to answer each of the following in a single request:
| Caller wants | Request |
|---|---|
A passwd record | Lookup{key_type: UnixId or Name, kind: Principal, fields: UNIX_ID | PRIMARY_GROUP | HOME | SHELL | DISPLAY_NAME} |
A group record | Lookup{key_type: UnixId or Name, kind: Group, fields: UNIX_ID | MEMBERS} |
| A principal's groups | Lookup{key_type: Name, kind: Principal, fields: GROUPS} |
| A name for a SID | Lookup{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.
| Field | Encoding | Limit |
|---|---|---|
tag | u32 | §2.14 |
kind | u8 | §2.B |
fields | u32 | §2.B |
of_key_type | u8 | §2.B |
of_name | string | 256 bytes |
of_sid | length-framed bytes (SID) | 68 bytes |
of_unix_id | u32 | |
cursor | length-framed bytes | 256 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.
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.
| Field | Encoding | Limit |
|---|---|---|
tag | u32 | §2.14 |
outcome | u8 | §2.B |
entries | array of entries | 256 |
next | length-framed bytes | 256 bytes |
incomplete | array of strings | 32 |
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.
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.
| Value | Name | Meaning |
|---|---|---|
| 1 | Found | The request was answered. |
| 2 | NotFound | No such object, and every source that could have said so was asked. |
| 3 | Unavailable | A source that could have answered did not. |
| 4 | Refused | The caller may not make this request. |
| 5 | Malformed | The 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.
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.
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.
| Field | Encoding | Limit |
|---|---|---|
identity | string | 128 |
service | string | 256 |
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:
- 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.
- 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.
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.
| Field | Encoding | Limit |
|---|---|---|
supported_credential_types | length-framed bytes, one u8 per type | 32 |
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 #
- Listen on
/run/logon.sockas aSOCK_STREAMUnix domain socket (§2.5). - Control access to it with a security descriptor, not with process integrity and not with POSIX permission bits (§2.4).
- Serve conversations concurrently, so that one stalled logon does not block others (§2.5).
- Support
SCM_RIGHTSon the channel (§2.9).
2.21.1.2 Framing #
- Reject any message whose magic is not
PGSL, as a whole-connection failure (§2.6). - Reject any message whose version it does not implement, answering
UnsupportedVersionwhere it can still encode a denial (§2.6). - Reject any message declaring more than 65536 bytes, or fewer than a header, without reading the body (§2.6).
- 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 #
- Require
LogonStart,CredentialChangeStart,CredentialEnrollStart,SessionEndorSessionEndQueryas the first message, and reject a conversation opening otherwise (§2.3). - Send exactly one terminal message, nothing after it, and close the connection (§2.3).
- Bound the number of rounds, the time spent awaiting an answer, and
the time a conversation may remain open, and terminate with
ConversationLimiton 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. - Reject a
CredentialResponseit did not solicit (§2.3). - 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 #
- Establish peer identity from the connected socket's peer token,
never from a message body, and never via
SO_PEERCRED(§2.4). - Treat
logon_typeas a proposal and constrain it against the verified peer (§2.4). - Treat
identifier,ttyandremote_hostas unverified claims (§2.4).
2.21.1.5 Derivation #
- Perform derivation itself, and never accept a token, privilege set or integrity level from another party (§2.1).
- Never fork or exec on the client's behalf; never learn about terminals, environments or session leadership (§2.1).
2.21.1.6 Prompting #
- 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). - Ensure
credential_refis unique among the prompts of one request (§2.8).
2.21.1.7 Result #
- Transfer the token as a file descriptor in the ancillary data of
AccessGranted, never by name (§2.9). - Never distinguish an unknown principal from a bad credential — by denial code, by reason text, or by timing (§2.10, §2.12).
- Send
homeandshellas 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 #
- 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).
- Never write credential material to a log, audit record, error message or diagnostic (§2.12).
2.21.1.9 Identity lookup #
- Listen on
/run/ident.sockas well as/run/logon.sock, as aSOCK_STREAMUnix domain socket (§2.14). - Grant connect access to that socket, by security descriptor, to every principal that runs ordinary programs (§2.14).
- Refuse a message of one socket's range received on the other (§2.14).
- Echo each request's
tag, and never interpret it otherwise (§2.14). - Serve requests concurrently, and never let a slow answer on one connection delay another (§2.14).
- Bound the time spent answering a request, and answer
Unavailablerather than waiting indefinitely (§2.14). - Ignore ancillary data received on the identity socket (§2.14).
2.21.1.10 Resolution #
- Interpret names itself, and never require a client to parse, qualify or case-fold one (§2.15).
- Compare names ASCII case-insensitively, establishing the comparison itself rather than delegating it to a source (§2.15).
- 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). - Resolve bare names in a configured order, never in registration order, stopping at the first source that answers (§2.15).
- 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 #
- Never answer a
Principalrequest with a group, or aGrouprequest with a principal, and establish the kind itself rather than relaying a source's (§2.16). - Ignore a field bit it does not implement, and never report it as present (§2.16).
- Report each requested field it implements as present or as withheld with a reason, never as neither (§2.16).
- Withhold
MEMBERSwith a reason rather than returning a partial or empty list, where it will not or cannot produce it (§2.16). - Report
Absentrather thanDeclinedfor a group whose membership is a rule rather than a record, and gather aBUILTINgroup's recorded members from the sources rather than reporting itAbsent(§2.16). - Encode "no POSIX identifier" as zero, and never substitute a number a caller could mistake for a real one (§2.16).
- Answer a
passwdrecord, agrouprecord, a principal's memberships, or a name for a SID, each in one request (§2.16). - Never report
NotFoundunless every source in the search order that could have answered was consulted and answered (§2.18). - Report
Unavailablewhere a source earlier in the search order could not be reached (§2.18). - Never report
NotFoundin place ofRefusedor of aRestrictedfield, and never reportRefusedfor anything other than a caller that may not make the request (§2.18). - List every source that did not contribute to an enumeration, and
never report
Foundon a page it could not produce (§2.17, §2.18). - 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). - Serve member enumeration where it withholds
MEMBERSasTooLarge(§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.
- Never hold, store, or accept a credential for a service identity, and never offer a credential exchange for one (§2.19).
- Refuse a
ServiceAttestunless 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). - Answer a peer that satisfies neither condition, or only one, with
PermissionDenied(§2.19). - Never mint a token under this message for a principal that has not
been designated as usable for a service logon, and refuse with
AccountRestrictedwhere it cannot determine whether a principal carries that designation (§2.19). - Never let a refusal distinguish "no such principal" from "not designated for service logon" (§2.19, §2.10).
- Reject an empty or unusable
servicewithMalformedRequest(§2.19). - Record the logon type as
Service, and leave everyprofilefield empty rather than inventing a value (§2.19, §2.9). - 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).
- Record that the logon was attested rather than authenticated, wherever it records what vouched for a credentialled logon (§2.19).
- Reject a
LogonStartthat would complete with no credential exchange for an identity it accepts underServiceAttest, and reject aServiceAttestnaming an identity it accepts underLogonStart(§2.19).
2.21.1.13 Logon type restrictions #
- 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). - Read an unstated
LOGON_TYPESas its own default rather than as "nothing permitted", and never includeServicein that default (§2.16, §2.19). - Never widen what a principal's holder permits. Local policy may narrow it further; nothing may add to it (§2.16).
- Refuse a
logon_typeit does not recognise, withMalformedRequest(§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.
- 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).
- Establish, within the conversation, that the principal can present
their current credential before changing it, and refuse with
AuthenticationFailedwhere they cannot (§2.20). - Refuse with
AccountRestricteda principal it holds no changeable credential for, including every service identity (§2.20, §2.19). - Send
CredentialChangedonly once the new credential is the one a subsequent logon will be tested against (§2.20). - Mint no token and create no session for a change, and send no
descriptor with
CredentialChanged(§2.20). - End with
ConversationLimita change it gives up on for want of an acceptable new credential (§2.20). - Refuse a
CredentialChangeStartthat ends beforesupported_credential_typeswithMalformedRequest(§2.20). - 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.
- Never accept
SessionEndQueryasSessionEndor the reverse, and change nothing in answeringSessionEndQuery(§2.22). - Answer
SessionEndQuerywithSessionEndAllowedexactly where aSessionEndfrom the same peer would be permitted, and otherwise with the denial it would draw (§2.22). - Refuse, with
PermissionDenied, a peer whose token is restricted, the SYSTEM and Anonymous sessions the kernel creates, and every session of logon typeService, whoever asks (§2.22). - 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).
- 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).
- Answer
NoSuchSessiononly to a caller who may end other principals' sessions, andPermissionDeniedto any other where the session does not exist (§2.22, §2.10). - 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).
- Address each signal by a reference that cannot come to name a different process, never by a process identifier looked up again (§2.22).
- Look again after each round, bound the number of rounds, and answer within 30 seconds (§2.22).
- Count in
remainingevery process it could not examine or end, and never fail the request for one (§2.22). - 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).
- Where the session is the one the connected peer's token belongs to,
send
SessionEndedbefore signalling anything, carrying what it found to end and aremainingof zero (§2.22). - 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.
- 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).
- For both
AddandRemove, establish within the conversation that the principal can present their current credential before changing anything, and refuse withAuthenticationFailedwhere they cannot (§2.23). - Refuse with
AccountRestricteda principal it holds no credential for that it can ask for as proof, including every service identity (§2.23, §2.19). - Refuse an
actionorcredential_typeit does not recognise withMalformedRequest, acredential_typeit does not enrol withPermissionDenied, and aCredentialEnrollStartthat ends beforematerialwithMalformedRequest(§2.23). - Refuse material it will not accept with
CredentialRejected, check it again when applying the change, and never sendCredentialRejectedfor a failed proof or in answer to any other message (§2.23, §2.B). - 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 #
- Send exactly one
LogonStart, as the first message (§2.3). - Answer each
CredentialRequestwith exactly oneCredentialResponse(§2.3). - Never send a
CredentialResponsethat was not solicited (§2.3). - Treat a connection that closes without a terminal message as a failed logon, and not retry automatically (§2.3).
2.21.2.2 Framing #
- Reject any message whose magic is not
PGSL(§2.6). - Reject any message whose version it does not implement (§2.6).
- Skip to a structure's declared end after reading known fields (§2.6).
2.21.2.3 Capabilities #
- Never declare in
supported_credential_typesa 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. - Fail the conversation, rather than guess, if it receives a prompt it cannot render (§2.8).
2.21.2.4 Rendering #
- Display received messages, in order, before the prompts of the same request (§2.8).
- Not interpret or branch on message text,
credential_name, orreason(§2.8, §2.10). - Echo
credential_refback unchanged (§2.8).
2.21.2.5 Result #
- Read the token descriptor from the ancillary data of
AccessGranted, and treat anAccessGrantedwithout one as a failed logon (§2.9). - Close the descriptor if it does not install the token (§2.9).
- Have a fallback for every
profilefield, and treat an empty field, and an absentprofile, identically and not as an error (§2.9). - Never execute a relative
shell, nor resolve a relativehomeagainst its own working directory (§2.9). Ashellcontaining no separator is relative, and MUST NOT be resolved against a search path. - Never treat
profileas 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 #
- Collect a credential only where it can establish that its collection
method meets the type's requirements, and never collect a
Passwordwith echo enabled (§2.12). - Never silently truncate an answer to fit its own buffer (§2.12).
- 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 #
- Forward a name unchanged, and never parse, split, qualify or case-fold one (§2.15).
- Never reuse a
tagwhile a request bearing it is outstanding, and never assume replies arrive in request order (§2.14). - Treat a cursor as opaque: never construct, parse or modify one, nor present one to a different authority or across a reconnection (§2.17).
- Continue an enumeration until
nextis 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). - Surface an enumeration's
incompletelist rather than discarding it (§2.17). - Distinguish
NotFoundfromUnavailable, and never cache the second as though it were the first (§2.18). - 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 #
- Send exactly one
CredentialChangeStart, as the first message, and name no principal in it — there is no field for one (§2.20). - Treat
CredentialChangedas 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 #
- Send exactly one
SessionEndorSessionEndQuery, as the only message, and useSessionEndQuery— neverSessionEnd— to learn whether ending a session would be permitted (§2.22). - Wait up to 60 seconds for the answer to
SessionEndbefore treating it as unanswered, and then not assume either outcome (§2.22). - Treat a non-zero
remainingas a session that may still exist, and never read the counts as an outcome where the session was the caller's own (§2.22). - Treat a denial code it does not recognise as a refusal, not as an unreadable answer (§2.B).
2.21.2.10 Credential enrolment #
- Send exactly one
CredentialEnrollStart, as the first message, with every field, and name no principal in it (§2.23). - Tell
CredentialRejected(the material) apart fromAuthenticationFailed(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.
| Field | Encoding | Limit |
|---|---|---|
logon_session_id | u64 | — |
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.
| Field | Encoding | Limit |
|---|---|---|
logon_session_id | u64 | — |
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.
- 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 isService— 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. - 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. - 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.
- No such session. Where the session does not exist, an authority
MUST answer
NoSuchSessionto a caller rule 3 permits, andPermissionDeniedto 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).
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.
2.22.7 SessionEnded #
msg_type = 0x8040. Authority to client. The successful terminal
message of SessionEnd; nothing follows it.
| Field | Encoding | Limit |
|---|---|---|
ended | u32 | — |
remaining | u32 | — |
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.
| Field | Encoding | Limit |
|---|---|---|
supported_credential_types | length-framed bytes, one u8 per type | 32 |
action | u8 | §2.B |
credential_type | u8 | §2.B |
material | string | 16384 |
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_type | Message | Direction | Defined in |
|---|---|---|---|
0x0001 | LogonStart | client → authority | §2.7 |
0x0002 | CredentialResponse | client → authority | §2.8 |
0x8001 | CredentialRequest | authority → client | §2.8 |
0x8002 | AccessGranted | authority → client | §2.9 |
0x8003 | AccessDenied | authority → client | §2.10 |
0x0020 | ServiceAttest | client → authority | §2.19 |
0x0030 | CredentialChangeStart | client → authority | §2.20 |
0x8030 | CredentialChanged | authority → client | §2.20, §2.23 |
0x0031 | CredentialEnrollStart | client → authority | §2.23 |
0x0040 | SessionEnd | client → authority | §2.22 |
0x0041 | SessionEndQuery | client → authority | §2.22 |
0x8040 | SessionEnded | authority → client | §2.22 |
0x8041 | SessionEndAllowed | authority → client | §2.22 |
2.A.1.2 On /run/ident.sock #
msg_type | Message | Direction | Defined in |
|---|---|---|---|
0x0010 | Lookup | client → authority | §2.16 |
0x0011 | Enumerate | client → authority | §2.17 |
0x8010 | LookupReply | authority → client | §2.16 |
0x8011 | EnumerateReply | authority → 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 #
| Constant | Value | Defined in |
|---|---|---|
| Logon socket path | /run/logon.sock | §2.5 |
| Identity socket path | /run/ident.sock | §2.14 |
| Magic | PGSL (50 47 53 4c) | §2.6 |
| Version | 1 | §2.6 |
| Header size | 12 bytes | §2.6 |
| Maximum message size | 65536 bytes | §2.6 |
| Longest grace before a forced end | 10 seconds | §2.22 |
Longest a SessionEnd may take to answer | 30 seconds | §2.22 |
| Least a client waits for that answer | 60 seconds | §2.21 |
2.A.3 Field limits #
| Field | Maximum | Defined in |
|---|---|---|
identifier | 1024 bytes | §2.7 |
tty | 128 bytes | §2.7 |
remote_host | 256 bytes | §2.7 |
supported_credential_types | 32 entries | §2.7 |
messages | 8 entries | §2.8 |
prompts | 16 entries | §2.8 |
answers | 16 entries | §2.8 |
text | 512 bytes | §2.8 |
credential_name | 128 bytes | §2.8 |
data | 32768 bytes | §2.8 |
home | 4096 bytes | §2.9 |
shell | 4096 bytes | §2.9 |
display_name | 256 bytes | §2.9 |
reason | 512 bytes | §2.10 |
material | 16384 bytes | §2.23 |
identity | 128 bytes | §2.19 |
service | 256 bytes | §2.19 |
name (lookup key) | 256 bytes | §2.16 |
sid | 68 bytes | §2.16 |
qualified_name | 512 bytes | §2.16 |
withheld | 32 entries | §2.16 |
values | 32 entries | §2.16 |
reference name | 512 bytes | §2.16 |
HOME, SHELL | 4096 bytes | §2.16 |
DISPLAY_NAME | 256 bytes | §2.16 |
GROUPS | 128 entries | §2.16 |
MEMBERS | 256 entries | §2.16 |
CLAIMS | 64 entries | §2.16 |
DESCRIPTION | 1024 bytes | §2.16 |
of_name | 256 bytes | §2.17 |
cursor, next | 256 bytes | §2.17 |
entries | 256 entries | §2.17 |
incomplete | 32 entries | §2.17 |
incomplete source name | 32 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.
| Value | Name |
|---|---|
| 2 | Interactive |
| 3 | Network |
| 4 | Batch |
| 5 | Service |
| 8 | NetworkCleartext |
| 9 | NewCredentials |
| 10 | RemoteInteractive |
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.
| Value | Name | identifier holds |
|---|---|---|
| 1 | Username | A 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.
| Value | Name | Collection |
|---|---|---|
| 1 | Password | A line of text, not echoed |
| 2 | SshPublicKey | Bound SSH candidate or signature proof; §2.D |
2.B.4 Message severities #
Carried in Message.severity (§2.8) as a u8.
| Value | Name |
|---|---|
| 0 | Info |
| 1 | Error |
2.B.5 Enrolment actions #
Carried in CredentialEnrollStart.action (§2.23) as a u8.
| Value | Name | material holds |
|---|---|---|
| 1 | Add | The credential to enrol: for SshPublicKey, one line of an OpenSSH public key file |
| 2 | Remove | Which 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.
| Value | Name | Meaning |
|---|---|---|
| 1 | MalformedRequest | The message could not be understood. |
| 2 | UnsupportedVersion | The protocol version is not implemented. |
| 3 | PermissionDenied | The peer may not make this request at all. |
| 4 | AuthenticationFailed | The principal is unknown, or the credential is wrong. Deliberately one code — see §2.10. |
| 5 | LogonTypeNotPermitted | The peer may not request this kind of session. |
| 6 | AccountRestricted | The 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). |
| 7 | AuthorityUnavailable | The authority cannot reach what it needs to decide. |
| 8 | ConversationLimit | Too many rounds, or too long without an answer. |
| 9 | Internal | The authority failed for a reason it will not describe. |
| 10 | NoSuchSession | There is no live logon session by that identifier. Answers only SessionEnd and SessionEndQuery (§2.22). |
| 11 | CredentialRejected | The 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.
| Value | Name | Key is in |
|---|---|---|
| 1 | Name | name |
| 2 | Sid | sid |
| 3 | UnixId | unix_id |
2.B.8 Object kinds #
Carried in Lookup.kind, LookupReply.kind_found and Enumerate.kind
(§2.16, §2.17) as a u8.
| Value | Name |
|---|---|
| 0 | Any |
| 1 | Principal |
| 2 | Group |
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.
| Bit | Name | Value encoding |
|---|---|---|
| 0 | UNIX_ID | u32 |
| 1 | PRIMARY_GROUP | reference |
| 2 | HOME | string |
| 3 | SHELL | string |
| 4 | DISPLAY_NAME | string |
| 5 | GROUPS | array of references |
| 6 | MEMBERS | array of references |
| 7 | CLAIMS | array of claim entries |
| 8 | ENABLED | u8 |
| 9 | LOGON_TYPES | u32 |
| 10 | DESCRIPTION | string |
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.
| Value | Name |
|---|---|
| 1 | Found |
| 2 | NotFound |
| 3 | Unavailable |
| 4 | Refused |
| 5 | Malformed |
2.B.11 Withheld reasons #
Carried in a withheld entry's reason (§2.16) as a u8.
| Value | Name | Meaning |
|---|---|---|
| 1 | Absent | The field has no value. |
| 2 | Restricted | The caller may not have this field. |
| 3 | Declined | The source will not produce it. |
| 4 | TooLarge | It 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 #
| Component | Responsibility |
|---|---|
| SSH client | Produces the standard SSH authentication signature using its private key |
| SSH transport and monitor | Establishes and retains the live transport binding; mediates credential collection |
| authd | Verifies the local originator, relays the credential exchange, enforces logon policy, and grants a token |
| lpsd | Owns 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:
| Field | Representation |
|---|---|
| Record ID | Random 128-bit identifier, stable until deletion |
| Public key | Validated, canonical SSH public-key blob; at most 8192 bytes |
| Label | UTF-8 text, at most 128 bytes; no control characters |
| Created time | UTC 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, initiallyPasswordand/orSshPublicKey. 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
keyornone, or a password policy with no password set — is refusedAccountRestricted, 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 addandlps key removeuse — 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 isCredentialRejected, with the store's reason; - names a key for removal by its
SHA256:fingerprint rather than the record IDlps key removetakes, because the fingerprint is what a person can read from their own.pubfile. 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:
| Field | Encoding and limit |
|---|---|
| Operation | u8: 0 Candidate, 1 Proof |
| Signature algorithm | UTF-8 string, 1–64 bytes |
| Public key | Byte string, 1–8192 bytes |
| Signature | Byte 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:
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:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | magic | MSIP (4d 53 49 50) |
| 4 | 2 | version | 1 |
| 6 | 2 | msg_type | See §3.A |
| 8 | 4 | total_len | Header 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:
| Field | Meaning |
|---|---|
element_types | The element types this surface can render. |
surface | Optional 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 repliesListing: 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:
reason | Meaning |
|---|---|
access_denied | The access check of §3.4 failed. |
unknown_kind | Start named a kind this daemon does not offer, or Attach named a conversation that does not exist or has ended. |
unsupported_elements | The conversation needs element types the surface did not declare. |
unavailable | The 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:
| Field | Meaning |
|---|---|
seq | This turn's sequence number. |
id | Optional template identity (§3.2). |
name | Optional display title. |
class | Optional presentation hints. |
elements | The 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:
| Field | Default | Meaning |
|---|---|---|
ref | — | Semantic identity (§3.2). Unique within the turn. |
type | — | An element type from the negotiated set. |
name | absent | Display label. |
class | [] | Presentation hints. |
help | absent | Longer explanatory text. |
required | false | An answer that validates must supply a value. |
default | absent | Value assumed when the answer omits this ref. |
secret | false | §3.4's handling rules apply. |
enabled | true | Disabled elements render but cannot be answered. |
error | absent | Validation 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 fromrequired, 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. actionis 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:
| Field | Meaning |
|---|---|
seq | The turn being answered. |
action | The action pressed, by ref. At most one. |
values | Input 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
defaultis 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 —
seqis 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
erroron 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:
| Field | Meaning |
|---|---|
seq | The outstanding turn. |
full | Replacement or patch. |
elements | See 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.
| Field | Meaning |
|---|---|
seq | The conversation's final seq. |
outcome | complete, cancelled, or failed. |
message | Optional 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.
code | Violation |
|---|---|
malformed | Undecodable body, or a field breaking this chapter's rules. |
unbound | A message requiring a binding, on an unbound connection — or a second binding attempt on a bound one. |
unknown_ref | An answer naming a ref the turn does not have. |
bad_value | A value on an action/disabled/output element, a wrongly-typed value, or an action rule of §3.9 broken. |
unexpected_type | A 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
Erroron 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,secretand 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).
| Message | Direction | msg_type |
|---|---|---|
| Hello | surface → daemon | 0x0001 |
| List | surface → daemon | 0x0002 |
| Start | surface → daemon | 0x0003 |
| Attach | surface → daemon | 0x0004 |
| Answer | surface → daemon | 0x0005 |
| Welcome | daemon → surface | 0x8001 |
| Listing | daemon → surface | 0x8002 |
| Bound | daemon → surface | 0x8003 |
| Refused | daemon → surface | 0x8004 |
| Turn | daemon → surface | 0x8005 |
| Update | daemon → surface | 0x8006 |
| Error | daemon → surface | 0x8007 |
| End | daemon → surface | 0x8008 |
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.
| State | Meaning |
|---|---|
text | The content. |
detail | What 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.
| State | Meaning |
|---|---|
min, max | Length bounds, in characters. |
pattern | Anchored regular expression the value must match. |
placeholder | Ghost 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.
| State | Meaning |
|---|---|
choices | Array of {value, name, help?}, in display order. |
multiple | Zero 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.
| State | Meaning |
|---|---|
columns | Array of {key, name, align?}, in display order. align is left (default) or right; numbers and sizes read better right-aligned. |
rows | Array 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. |
empty | Text 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.
| State | Meaning |
|---|---|
value | Progress so far. |
max | The whole; absent means indeterminate. |
3.B.7 log — output #
Accumulating lines of detail.
| State | Meaning |
|---|---|
lines | The lines so far. |
append | In 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 #
| State | Meaning |
|---|---|
validate | Default true; false skips §3.9 validation. |
primary | The action an unattended answerer presses. At most one per turn. |
destructive | Pressing 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:
| Form | Meaning | Defined in |
|---|---|---|
generic | The 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.
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.
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:
| Key | Value | Type | Meaning |
|---|---|---|---|
Machine\Generic\Icons | Themes | REG_MULTI_SZ | Theme 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.
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.
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:
- The whole type,
<type>/<subtype>, with/replaced by-. - 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 is | Type |
|---|---|
| A directory | inode/directory |
| A regular file that may be executed, and nothing better is known | application/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.
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
genericwhere 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\IconsThemes, 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, orgeneric(§4.5). - Finds
.pgss.iconby 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>.svgunder/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 inuser.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 #
| Identifier | Drawn for |
|---|---|
generic | Anything 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.
| Identifier | Drawn for |
|---|---|
inode-directory | A directory |
application-x-executable | A program |
application | A file of any other application/ type |
text | A file of any text/ type |
image | A file of any image/ type |
audio | A file of any audio/ type |
video | A 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).
| Identifier | Drawn for |
|---|---|
directory-documents | A directory called Documents |
directory-downloads | A directory called Downloads |
directory-pictures | A directory called Pictures |
directory-music | A directory called Music |
directory-videos | A 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.
5.3.2 The file #
The file is a TOML document whose top-level table has these keys:
| Key | Type | Meaning |
|---|---|---|
title | string | What the application is called. One line, not empty. Required. |
program | string | The file to run: an absolute path. Required. |
arguments | array of strings | What the program is always started with, in order, before anything else. Empty when absent. |
icon | string | The identifier of its icon (PGSS Icons §4.3). The id when absent. |
description | string | One line on what it is for. None when absent. |
opens | array of strings | The media type patterns it opens. Empty when absent. |
listed | boolean | Whether a launcher lists it. true when absent. |
An example, complete for a file explorer:
= "Gexora"
= "Browse the files on this system"
= "/usr/bin/gexora"
= ["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
titleis absent, is not a string, is empty or is more than one lineprogramis 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
iconthat is not in the form of an identifier is treated as absent, and the icon is the id - an entry in
opensthat is not a media type pattern is left out - leading and trailing whitespace on
title,descriptionandiconis 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.
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.
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.
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:
| Key | Whose choices |
|---|---|
CurrentUser\Generic\Applications\Defaults | The person's. The kernel routes CurrentUser\ to the caller's own Users\<SID>\, so each person has their own. |
Machine\Generic\Applications\Defaults | The machine's: everyone's on it, for whatever they have not chosen themselves. |
Each value of either key is one choice:
| Part | Is |
|---|---|
| Name | A media type pattern (§5.2): the type the choice is for, image/png, or a top-level type and *, image/*. |
| Type | REG_SZ. |
| Data | The 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:
- the person's choice named
type/subtype - the person's choice named
type/* - the machine's choice named
type/subtype - 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.
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.
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
Defaultsunder eitherApplicationskey. 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>.tomlwith 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
iconas 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
titleand an absoluteprogram, 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\Defaultswhere 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.
| Role | Obligation |
|---|---|
| Emitter | Writes 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). |
| Consumer | Reads 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:
| Notation | Meaning |
|---|---|
and, or, not | Boolean operators |
for each x in list | Iteration in list order |
absent | No value: a key or registry value that does not exist |
return | Ends 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.
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.
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.
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.
| Role | Participant |
|---|---|
subject | Whose authority the action used: the principal that acted |
object | What the action was done to |
source | Where something came from: the origin of a copy, of traffic, of data a backend supplied |
destination | Where it went |
emitter | Who 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):
| Root | Holds |
|---|---|
subject, object, source, destination, emitter | The participants, and their common things: token, process, file, key, session |
event | The record's own header values (below) |
access | Rights asked for, granted and audited |
outcome | Whether the action succeeded, and why not (§6.6) |
trigger | Whatever caused the record to exist: an audit ACE, a blocking layer, a causing file |
operation | What was attempted and how |
config | A configuration value reported on |
policy | A rule set in force |
transaction | A transaction an operation belonged to |
fields | Properties 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 path | Type |
|---|---|---|
type | event.type | str |
timestamp | event.time | uint.time |
sequence | event.sequence | uint |
cpu_id | event.cpu | uint |
| — | event.boot.guid | bin.guid |
origin_class | emitter.class | uint.enum |
effective_token_guid | emitter.token.guid | bin.guid |
true_token_guid | emitter.true-token.guid | bin.guid |
process_guid | emitter.process.guid | bin.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:
| Path | Value |
|---|---|
policy.generation | The generation now in force |
policy.generation-previous | The generation it replaced |
object.key.path | The key's resolved path |
object.key.path-requested | The 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).
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 #
| Value | Wire form | Type |
|---|---|---|
| Principal | A binary SID (PCDS) | bin.sid |
| GUID | The 16-byte binary GUID (PCDS) | bin.guid |
| LUID | Unsigned integer | uint.luid |
| ACE | The binary ACE (PCDS) | bin.ace |
| Point in time | Unsigned integer, nanoseconds since the Unix epoch | uint.time |
| Duration | Unsigned integer, nanoseconds | uint.duration |
| Size in bytes | Unsigned integer | uint.bytes |
| Access mask | Unsigned integer | uint.mask |
| Flags | Unsigned integer | uint.flags |
| Integrity level | Unsigned integer, the integrity RID | uint.integrity |
| Error number | Signed integer, negative | int.errno |
| Boolean | MessagePack boolean | bool |
| Enumeration | String, kebab-case | str.enum |
| Numeric enumeration | Unsigned integer | uint.enum |
| Path | String | str.path |
| IP address | String, in the textual form for its family | str.ip |
| Digest | Binary | bin |
| Text | String | str |
| Other integer | Unsigned or signed integer | uint, 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, neveroperation.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.
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 #
| Field | Type | Carries |
|---|---|---|
outcome.success | bool | Whether the action did what was asked |
outcome.reason | str.enum | Why it failed, or why a success was unusual |
outcome.errno | int.errno | The error number it failed with |
outcome.detail | str | Text 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.
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:
| Key | Value |
|---|---|
kind | The 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.
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:
- 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.
- 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.
6.8.2 Tiers #
Every event type declares one tier in its fragment (§6.10). The tiers are a closed set.
| Tier | Meaning | On unless the policy says otherwise |
|---|---|---|
essential | Extremely important and relatively rare, or governed by its own configuration | Always; cannot be switched off |
standard | What an administrator reviews: lifecycle, configuration, failures | Yes |
verbose | Detail for an active investigation | No |
debug | For the component's developers | No |
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.
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
Enabledvalue on the deepest key that has one decides. A value onEventsitself 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
Enabledvalue on the path, astandardtype is on, and averboseordebugtype 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 type | Tier | Decided by | On |
|---|---|---|---|
lcs.config.value.rejected | standard | its tier | Yes |
kacs.caap.staging.diverged | standard | Events\kacs | No |
kacs.audit.handle.used | standard | Events\kacs\audit | Yes |
kacs.audit.access.checked | essential | its tier, always | Yes |
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.
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:
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 #
| Key | Required | Value |
|---|---|---|
type | Yes | The field's type (§6.5) |
values | For an enumeration | The values, separated by |, or the name of the reason-code family they are drawn from |
closed | For an enumeration | true if no value may be added without a new version of the field |
asserted | No | true if the value may be supplied by userspace on a kernel-originated record (§6.7) |
carried | No | header 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 #
| Key | Required | Value |
|---|---|---|
tier | Yes | The event type's tier (§6.8) |
gating | If any | What decides which occurrences are recorded |
cardinality | No | How 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:
- Every path a
field:orinclude:line names is defined. - No path is defined by two fragments, and no path is defined twice in one.
- No path is defined both as a value and as a map (§6.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.
- 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).
- 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.
- Every enumeration declares its values or names its reason-code family, and says whether it is closed.
- Every event type declares a tier, and an event type with gating declares it.
- An event type that carries a
uint.maskfield carriesobject.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.
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 carriesoutcome.successwherever 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.kindwherever 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
.evmanfile 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.
| Root | Component | Package |
|---|---|---|
kacs | Kernel Access Control Subsystem | dev.peios.kernel |
lcs | The kernel's registry (configuration store) | dev.peios.kernel |
kmes | Kernel Mediated Event Subsystem | dev.peios.kernel |
stratafs | StrataFS | dev.peios.kernel |
ntfe | The kernel's network-policy engine | dev.peios.kernel |
peinit | The service manager | dev.peios.peinit |
peipkg | The package manager | dev.peios.peipkg |
eventd | The event store | dev.peios.eventd |
authd | The authentication daemon | dev.peios.authd |
lpsd | The local principal source | dev.peios.authd-lpsd |
timed | The time daemon | dev.peios.timed |
netd | The network daemon | dev.peios.netd |
trustd | The trust daemon | dev.peios.trustd |
These roots are reserved for components that write no events yet, and MUST NOT be used by anything else:
| Root | Component |
|---|---|
resolvd | The name-resolution daemon |
ud | The user daemon |
loregd | The log store |
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.