KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

SPEC-RPC — kit cross-system wire protocol v1

kit speaks to processes outside its address space — external signers running as subprocesses, and remote kit serve instances reached over ssh(1) — using a single shared wire protocol.

The schemas live in rust/crates/kit-rpc/proto/:

File Purpose
common.proto Shared types: Algorithm, KeyForm, ErrorCode, Error, ProtocolVersion
signer.proto External-signer protocol (SignerFrame oneof). See SPEC-EXTERNAL-SIGNER for prose.
ssh.proto SSH transport protocol (SshFrame oneof). See SPEC-TRANSPORT for prose.
verify.proto Object-signature verification contract (VerifyRequest/VerifyResponse). Not a framed protocol under §1/§4 below — it is message-only, with no oneof-dispatched frame and no bound RPC method yet. kit-bin's local dispatch (rust/crates/kit-bin/src/remote_dispatch/packmap.rs) calls the Rust implementation directly (kit_core::sign::{verify_commit,verify_remix,verify_tag}); the schema exists so a future ConnectRPC transport can bind the identical check to a service method instead of reimplementing it.

ssh.proto imports the CAS enum (RefExpectation) and the ref-listing type (RefEntry) from kit/common/v1/refs.proto at the repo root — a vocabulary shared with kit.transport.v1, so the two wire protocols cannot desync on CAS semantics. See that file's header comment for its own versioning contract.

Protocol-version integer is 1 (PROTOCOL_VERSION_1) — the only value kit currently speaks. Wire-breaking changes (renumbering or removing a field) are made by introducing a sibling signer2.proto / ssh2.proto package and bumping to a new ProtocolVersion value, never by silently reinterpreting PROTOCOL_VERSION_1's existing wire shape (see §2).

Distribution. The schemas live in rust/crates/kit-rpc/proto and proto/. Integrators generate typed bindings in their own language from these files. They do not need to vendor this repository.


1. Wire framing

Both protocols use the same length-prefixed framing:

+------------------+------------------------------------+
| u32 LE length    | N bytes protobuf-encoded Frame     |
+------------------+------------------------------------+

Receivers MUST validate the length prefix against the cap before allocating buffers — there is no streaming decode of a single frame.

A truncated length prefix or body is a connection-fatal protocol error. There is no recovery; the receiving side MUST close the underlying stream.


2. Versioning

The single ProtocolVersion enum in common.proto carries the wire contract version. Every initiating frame (SignerFrame::Hello, SshFrame::Hello) MUST set protocol = PROTOCOL_VERSION_1.

Wire-compatible additions to the v1 schemas are allowed in v0.1.x patch releases:

Renumbering or removing a field is a wire break. Do that by introducing a sibling *2.proto with a new package and bumping ProtocolVersion.


3. Common types

3.1 Algorithm

Wire-load-bearing integers; do NOT renumber. New algorithms append at the next available number.

Name Wire Notes
ALGORITHM_UNSPECIFIED 0 Treated as "request invalid".
ALGORITHM_ED25519 1 RFC 8032 raw signing. Default for commit/ref.
ALGORITHM_SECP256K1 2 ECDSA over SHA-256. DSSE-compatible.
ALGORITHM_P256 3 ECDSA P-256/secp256r1/prime256v1 over SHA-256.
ALGORITHM_ED25519_WEBAUTHN 4 Ed25519 wrapped in a WebAuthn assertion (WebAuthn signers).
ALGORITHM_BLS12381_THRESHOLD 5 BLS12-381 threshold signature (variant MinSig); see docs/specs/SPEC-RELEASE-THRESHOLD.md.

3.2 KeyForm

Name Wire Used by
KEY_FORM_UNSPECIFIED 0 —
KEY_FORM_RAW_BYTES 1 kit-sign-file (32-byte seed/scalar on disk).
KEY_FORM_PKCS8_DER 2 Reserved; no signer ships with v0.1.0.
KEY_FORM_OPAQUE_HANDLE 3 No signer ships. The signer interprets the bytes in key_ref (for example a TPM persistent handle or a CTAP credentialId).

3.3 ErrorCode

Name Wire Meaning
ERROR_CODE_UNSPECIFIED 0 Default/unset. Receivers MUST treat this as a protocol error (every Error frame MUST carry a known non-zero code).
ERROR_CODE_INVALID_REQUEST 1 Structurally bad request. Connection MAY continue.
ERROR_CODE_UNSUPPORTED_ALGORITHM 2 Signer doesn't speak the algorithm.
ERROR_CODE_UNSUPPORTED_KEY_FORM 3 Signer doesn't accept the key form.
ERROR_CODE_KEY_NOT_FOUND 4 Named key (handle/path) was absent.
ERROR_CODE_USER_DECLINED 5 User canceled (touched cancel on hardware key, refused TPM auth).
ERROR_CODE_AUTHENTICATION_REQUIRED 6 Signer needs PIN/biometric — see PinPrompt.
ERROR_CODE_HARDWARE_ERROR 7 Vendor-specific hardware fault; details carries the raw code.
ERROR_CODE_TIMEOUT 8 User presence check timed out.
ERROR_CODE_INTERNAL 99 Unmapped — message MUST be set.

Error.message is human-readable English, suitable for direct UI display. Producers SHOULD keep it under 1 KiB so receivers can render it inline; the framing layer does NOT enforce a per-field cap, only the MAX_FRAME_BYTES = 1 MiB whole-frame ceiling from §1. Error.details is opaque vendor-specific bytes (CTAP CBOR status, TPM TPM_RC, PKCS#11 CKR_*, etc.); programs MUST NOT pattern-match on its contents.


4. Conformance

A signer or server is kit-rpc-compliant if and only if:

  1. It speaks length-prefixed protobuf frames as in §1.
  2. The first frame it sends or expects is Hello with protocol = PROTOCOL_VERSION_1.
  3. It returns a HelloResponse with the same protocol value before processing any other request.
  4. Every error response is an Error frame with a known non-zero ErrorCode and a non-empty message.
  5. It enforces MAX_FRAME_BYTES on receive AND on send.

Reference implementations (all in this repository):


5. Why protobuf, not JSON

kit-rpc replaces an earlier line-JSON external-signer protocol and a hand-rolled OP_HELLO byte format on the SSH transport. The protobuf choice is deliberate:

The Rust reference implementations currently generate their protobuf bindings with the buffa runtime — this is a reference-implementation detail, not a specification requirement, and this document does not pin a version (a version pinned in prose drifts silently from whatever the workspace actually builds against). Other languages, and other Rust implementations, can use any protobuf 3/edition 2023 toolchain that emits the wire bytes defined by the .proto files listed at the top of this document (§1); conformance is defined by those bytes, never by which codegen tool produced them.


6. Invariants

Invariant Enforced by
No frame exceeds 1 MiB, in either direction MAX_FRAME_BYTES checked on receive AND send; conformance requires both (§1, §4)
A hostile length prefix cannot force a large allocation length validated against the cap before buffers are allocated; no streaming decode of a single frame (§1)
A truncated frame is never mis-parsed as a shorter one truncation is connection-fatal; the receiver MUST close the stream, no recovery (§1)
No request is processed before version agreement first frame MUST be Hello with PROTOCOL_VERSION_1; HelloResponse echoes it before any other processing (§2, §4)
Every error is machine-actionable Error frames MUST carry a known non-zero ErrorCode and non-empty message; ERROR_CODE_UNSPECIFIED is itself a protocol error (§3.3, §4)
A receiver never mistakes a missing ErrorCode for an ordinary unmapped one kit_rpc::helpers::rpc_error_to_transport (shared by every SshFrame-based transport client) classifies code = 0/absent code as TransportError::ProtocolError, distinct from the generic RemoteError a legitimate-but-unmapped code produces (§3.3, §4)
Wire numbers never change meaning Algorithm/KeyForm/ErrorCode integers are append-only, never renumbered; field renumbering/removal requires a sibling *2.proto + ProtocolVersion bump (§2, §3.1)
v1 additions cannot break existing peers new enum values fall back to unknown-variant handling; new oneof variants are rejected with ERROR_CODE_INVALID_REQUEST (§2)
Bulk data never violates the frame cap flows over 1 MiB use the streaming pattern (PackChunk) instead of a bigger frame (§1)
Programs never couple to vendor-opaque bytes Error.details MUST NOT be pattern-matched (§3.3)

The conformance checklist in §4 is the normative test of these properties; the .proto files remain the source of truth (§5).