pawsd:protocol
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| pawsd:protocol [2026/02/07 20:02] – link transports winter | pawsd:protocol [2026/04/29 17:25] (current) – [Verb 4098 (0x1002): Fetch contact] formatting winter | ||
|---|---|---|---|
| Line 6: | Line 6: | ||
| - | The PawSD protocol is designed around a simple request-response architecture that allows it to work over a variety of underlying [[transports]]. All a transport has to provide is a way for a client to send some bytes to a server and for the server to send some bytes back in response. | + | The PawSD protocol is designed around a simple request-response architecture that allows it to work over a variety of underlying [[transports]]. All a transport has to provide is a way for a client to send some bytes to a server and for the server to send some bytes back in response. It is imperative to choose transports that provide some form of data integrity, i.e. preventing malicious third parties from modifying data in-transit between the client and server. |
| Unless otherwise specified, the protocol uses big-endian byte and bit order, and no padding. | Unless otherwise specified, the protocol uses big-endian byte and bit order, and no padding. | ||
| Line 61: | Line 61: | ||
| The signature is calculated from the concatenation of the index, flags, record count, and records fields (i.e. the entire service in wire format except for the signature itself). The signature algorithm is not given in the service itself but will be specified as part of the service' | The signature is calculated from the concatenation of the index, flags, record count, and records fields (i.e. the entire service in wire format except for the signature itself). The signature algorithm is not given in the service itself but will be specified as part of the service' | ||
| + | |||
| + | The " | ||
| Records are just a list of tag-value pairs: | Records are just a list of tag-value pairs: | ||
| Line 88: | Line 90: | ||
| ===== Verb 1: Fetch service ===== | ===== Verb 1: Fetch service ===== | ||
| - | The client asks the server for a particular service. Even if the service exists, the server may not have it, in which case it should | + | The client asks the server for a particular service. Even if the service exists, the server may not have it, in which case it may respond " |
| Request payload: | Request payload: | ||
| Line 96: | Line 98: | ||
| | public key | varies | | public key | varies | ||
| | service index | 2 bytes | u16 | | | service index | 2 bytes | u16 | | ||
| + | |||
| + | Design note: although the length of the public key is implicitly given by the key type field (e.g. an ed25519 key will always be 32 bytes long), we still explicitly state its length, so that implementations that don't support a given key type can still parse the request properly, even if just to show a more useful error message. | ||
| Response payload: | Response payload: | ||
| ^ Field ^ Length ^ Type ^ | ^ Field ^ Length ^ Type ^ | ||
| | service | varies | service | | | service | varies | service | | ||
| + | |||
| + | |||
| + | ===== Verb 2: Query services ===== | ||
| + | |||
| + | The client sends a structured search query to the server, which it performs recursively (up to the specified maximum depth, or until its own depth limit) against all zones it has cached locally. Generally the server shouldn' | ||
| + | |||
| + | Request payload: | ||
| + | ^ Field ^ Length | ||
| + | | starting zone | varies >4 | key type, length, key (as in first three fields of verb 1) | | ||
| + | | max recursion depth | 1 byte | u8 | | ||
| + | | query | varies | ||
| + | |||
| + | Recursion starts at the " | ||
| + | |||
| + | The query itself is a tree-like expression, representing a logical combination of comparisons against properties of the service or its records. The <wrap important> | ||
| + | |||
| + | <code bnf> | ||
| + | service-condition = service-logical / service-comparative | ||
| + | / all-records / any-records | ||
| + | |||
| + | ;; boolean operators (T is service, record, or tag) | ||
| + | T-logical = T-not / T-and / T-or | ||
| + | T-not = %x00 T-condition | ||
| + | T-and = %x01 T-condition T-condition | ||
| + | T-or = %x02 T-condition T-condition | ||
| + | |||
| + | ;; numeric operators (=, <, >) (T as above) | ||
| + | T-comparative = T-equals / T-lt / T-gt | ||
| + | ;; for all T-lt and T-gt, the specified value is the | ||
| + | ;; RHS and the value in the T is the LHS | ||
| + | |||
| + | u8 = OCTET | ||
| + | u16 = u8 u8 | ||
| + | u32 = u16 u16 | ||
| + | u64 = u32 u32 | ||
| + | tag = u64 u64 | ||
| + | string = u32 *(u8) ;; length prefixed string, *=u32 | ||
| + | |||
| + | ;; returns true iff service index = ' | ||
| + | service-equals = %x03 service-index | ||
| + | service-index = u16 | ||
| + | ;; comparisons for the number of records in a service | ||
| + | service-lt = %x04 record-count | ||
| + | service-gt = %x05 record-count | ||
| + | record-count = u16 | ||
| + | |||
| + | ;; both map a condition over all elements of the parent | ||
| + | ;; returns true iff all matched | ||
| + | all-Ts = %x06 T-condition | ||
| + | ;; returns false iff none matched | ||
| + | any-Ts = %x07 T-condition | ||
| + | |||
| + | record-condition = record-logical / record-comparative | ||
| + | / all-tags / any-tags | ||
| + | |||
| + | record-lt = %x08 tag-count | ||
| + | record-gt = %x09 tag-count | ||
| + | tag-count = u16 | ||
| + | |||
| + | tag-condition = tag-logical / tag-comparative | ||
| + | |||
| + | tag-equals = %x0A tag ;; tag ID = ? | ||
| + | / %x0B string ;; tag value = ? | ||
| + | / %x0C string ;; tag value substring match | ||
| + | / %x0D string ;; tag value fuzzy match | ||
| + | |||
| + | ;; compare tag value as an unsigned int | ||
| + | tag-lt = %x0E tag-xt | ||
| + | tag-gt = %x0F tag-xt | ||
| + | tag-xt = %x01 u8 / %x02 u16 / %x04 u32 / %x08 u64 / %x10 u64 u64 | ||
| + | </ | ||
| + | |||
| + | The response is a list of matching services grouped by zone: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | zone count | 2 bytes | u16 | | ||
| + | | zone< | ||
| + | | zone< | ||
| + | | // | ||
| + | |||
| + | ==== Examples ==== | ||
| + | |||
| + | To fuzzy-search for the string " | ||
| + | * construct a logical query something like '' | ||
| + | * in a lisp-like syntax you might write that as ''< | ||
| + | * encode it accordingly: | ||
| + | * the top-level " | ||
| + | * so to match against any record, use '' | ||
| + | * inside that condition, the parent is a record, whose elements are tags | ||
| + | * so we use the same again to match against any tag | ||
| + | * inside the sub-condition we want to fuzzy match the tag's value: '' | ||
| + | * the string itself will just be length-prefixed UTF-8: '' | ||
| + | * which results in the final encoded query '' | ||
| Line 106: | Line 203: | ||
| </ | </ | ||
| + | |||
| + | ===== Verb 4096 (0x1000): Start authentication ===== | ||
| + | |||
| + | The client briefly identifies itself using a " | ||
| + | |||
| + | Request payload: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | client ID | 16 bytes | [16]u8 | ||
| + | | scopes | ||
| + | |||
| + | The server then returns either a " | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | completed? | ||
| + | | token or challenges | ||
| + | |||
| + | If " | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | conjunction | ||
| + | | challenge count | 1 byte | u8 (≥ 1) | | ||
| + | | challenge< | ||
| + | | challenge< | ||
| + | | challenge< | ||
| + | | // | ||
| + | |||
| + | If the conjunction is " | ||
| + | |||
| + | |||
| + | ==== Challenges ==== | ||
| + | |||
| + | The structure of the " | ||
| + | |||
| + | |||
| + | ===== Verb 4097 (0x1001): Continue authentication ===== | ||
| + | |||
| + | The client sends one or multiple responses to challenges previously given by the server. (Note confusing terminology: | ||
| + | |||
| + | Request payload: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | client ID | 16 bytes | [16]u8 | ||
| + | | response count | 1 byte | u8 | | ||
| + | | challenge< | ||
| + | | response< | ||
| + | | response< | ||
| + | | // | ||
| + | |||
| + | The response payload is identical to the //Start authentication// | ||
| + | |||
| + | |||
| + | ===== Verb 4098 (0x1002): Fetch contact ===== | ||
| + | |||
| + | The client sends a zone's public key to be looked up in the authenticated user's contact list. (" | ||
| + | |||
| + | Request payload: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | auth token | 32 bytes | [32]u8 | | ||
| + | | public key type | 2 bytes | enum | | ||
| + | | public key length | 2 bytes | u16 | | ||
| + | | public key | varies | ||
| + | |||
| + | The server will either respond " | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | local name length | 2 bytes | u16 | | ||
| + | | local name | varies | ||
| + | | trust level | 1 byte | enum | | ||
| + | | introducer zone | varies | ||
| + | |||
| + | The trust level can be one of the following values: | ||
| + | * 0 = **distrusted**: | ||
| + | * 1 = **neutral**: | ||
| + | * 2 = **introduced**: | ||
| + | * 3 = **trusted**: | ||
| + | |||
| + | The introducer zone field may be set to the public key of a zone that points to the requested zone, as an explanation for the trust level being " | ||
| + | |||
| + | |||
| + | ===== Verb 4099 (0x1003): Put contact ===== | ||
| + | |||
| + | The client sends a zone's public key, local name, trust level and optionally an introducer zone, to be stored by the server in the authenticated user's contacts. If the zone in question was already in the contacts, its entry will be modified to the new information. | ||
| + | |||
| + | Request payload: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | auth token | 32 bytes | [32]u8 | ||
| + | | public key type | 2 bytes | enum | | ||
| + | | public key length | 2 bytes | u16 | | ||
| + | | public key | varies | ||
| + | | local name length | 2 bytes | u16 | | ||
| + | | local name | varies | ||
| + | | trust level | 1 byte | enum | | ||
| + | | introducer zone | varies | ||
| + | |||
| + | Response payload: | ||
| + | |||
| + | ^ Field ^ Length ^ Type | | ||
| + | | updated? | 1 byte | boolean | | ||
| + | |||
| + | " | ||
| + | |||
| + | |||
| + | ===== Verb 4100 (0x1004): Remove contact ===== | ||
| + | |||
| + | The client sends a zone's public key, to be removed from the authenticated user's contact list. | ||
| + | |||
| + | Request payload: | ||
| + | |||
| + | ^ Field ^ Length | ||
| + | | auth token | 32 bytes | [32]u8 | | ||
| + | | public key type | 2 bytes | enum | | ||
| + | | public key length | 2 bytes | u16 | | ||
| + | | public key | varies | ||
| + | |||
| + | Response payload is empty. A successful status code indicates that the contact was deleted, whereas if the contact didn't exist in the first place, the status should be " | ||
pawsd/protocol.1770494562.txt.gz · Last modified: by winter
