-*- outline -*- Rationale for new protocol, not using XML-RPC/whatever: We need something simple and small that we can easily write a thorough verifier for. The need to transfer authenticated data without any risk of attacks (e.g. replay of older authenticators) also requires a protocol defined at the byte level. CONVENTIONS ----------- * All numbers are in network byte order. * bool == u32, only 0 and 1 allowed * strings do not have a trailing NUL unless specified otherwise bytes \x00..\x2F, \x7F are prohibited STREAM PROTOCOL --------------- Both connections to the bridge use TLS, and the TLS session is used to transfer two separate "streams": "outer", which contains data accessible to the bridge, and "inner", a nested TLS session. The TLS connection between an endpoint and the bridge consists of a sequence of chunks with the following format: * u32 N length/type: ** bits 0..30: L length of the following chunk. if L == 0, this is an unidirectional "end of stream" marker. ** bit 31: 0 == outer stream, 1 == inner stream * u8[L] chunk data The order of inner and outer chunks does not need to correspond to the logical order (if the protocols requests sending outer data, then inner, the inner chunk may actually be sent over the TLS session before the outer chunk). The implementation nevertheless should not delay a chunk indefinitely just because a lot of data is being transfered over the other stream. (This simplifies implementation because it is not necessary to communicate chunk ordering to the subprocess used to implement the nested TLS sessions.) ENDPOINT PROTOCOL ----------------- The endpoints (client and server) use the stream protocol to talk to the bridge, which forwards the communication between them. The inner stream uses a nested TLS session, uses of the inner stream are marked with "|" characters. * request: TLS to bridge / from bridge ** u32 version ID == 0 ** u8 N number of key-value pairs N <= 255 ** N times: *** u8 L key length L <= 255 *** u8[L] key: string *** u8 L value length L <= 255 *** u8[L] value ** u32 L payload length FIXME: limit request-specific ** u8[L] payload ** client sends EOF on the outer stream at this point ** |TLS: client - server initialized ** |u8 N number of key-value pairs |N <= 255 ** |N times: *** |u8 L key length |L <= 255 *** |u8[L] key: string *** |u8 L value length |L <= 255 *** |u8[L] value ** |now both sides close the inner stream and send EOF on it * reply: ** u32 error code - 0 OK, others defined ** u8 N number of key-value pairs N <= 255 ** N times: *** u8 L key length L <= 255 *** u8[L] key: string *** u8 L name length L <= 255 *** u8[L] value ** u32 L payload length ** u8[L] payload ** server sends EOF on the outer stream at this point COMMON REQUEST FIELDS --------------------- all mandatory ** op: string ** user: string ** |header-auth-sha512: blob, all up to payload (including "payload length") The hash is computed over the stream content, ignoring chunk headers. ** |payload-auth-sha512: blob (FIXME: rpm) The hash is computed over the stream content, ignoring chunk headers. ERROR RESPONSE FIELD -------------------- ** message: string ADMIN REQUESTS -------------- * common fields: ** |password: blob The request must satisfy: user.admin && password matches user.sha512_password * op="list-users" ** => ** num-users: u32 ** _payload_: num-users * NUL-terminated user name * op="new-user" ** name: string ** [|new-password: blob] default: password unset ** [admin: bool] default false ** => * op="delete-user" ** name: string ** => * op="get-user-properties" ** name: string ** => ** admin: bool * op="set-user-properties" ** name: string ** [admin: bool] ** [new-name: string] ** [|new-password: string] ** => default values: no change * op="get-key-user-properties" ** name: string ** key: string ** => ** key-admin: bool * op="set-key-user-properties" ** name: string ** key: string ** [key-admin: bool] ** => default values: no change * op="list-keys" ** => ** num-keys: u32 ** _payload_: num-keys * NUL-terminated key name * op="new-key" ** name: string ** |passphrase: blob ** [initial-key-admin]: string defaults to the invoking user ** FIXME: other parameters ** => ** _payload_: ASCII armored public key * op="get-public-key" ** name: string ** => ** _payload_: ASCII armored public key * op="delete-key" ** name: string ** => * op="set-key-properties" ** name: string ** [new-name: string] ** => KEY ADMIN REQUESTS ------------------ * common fields: ** key: string ** |passphrase: string The request must satisfy: (key_access[key,user].admin) && passphrase matches key.encrypted_passphrase * op="list-key-users" Access allowed also to admins (see "admin requests" for access rule) ** => ** num-users: u32 ** _payload_: num-users * NUL-terminated user name * op="grant-key-access" ** name: string ** |new-passphrase ** => * op="revoke-key-access" Access allowed also to admins (see "admin requests" for access rule) ** name: string ** => USER REQUESTS ------------- * common fields: ** key: string ** |passphrase: string The passphrase must match key.encrypted_passphrase * op="change-passphrase" ** |new-passphrase: string * op="sign-text" ** _payload_: data to sign ** => ** _payload_: data with clear-text signature * op="sign-data" ** _payload_: data to sign ** => ** _payload_: detached signature packet * op="sign-rpm" modified by bridge to supply complete payload ** rpm-name: string ** rpm-epoch: string ** rpm-version: string ** rpm-release: string ** rpm-arch: string ** import-signature: bool default: true ** return-data: bool default: false ** _payload_: complete RPM, added by bridge if empty FIXME: can we extract only the header and authenticate it safely? Perhaps by "using" rpm --addsign to get the data? ** => ** _payload_: signed RPM imported into koji by bridge if import-signature dropped by bridge if !return-package