signer.proto
In rust/crates/mkit-rpc/proto/mkit/rpc/v1/signer/
Preview
Binary · 8.1 KiB// SPDX-License-Identifier: MIT OR Apache-2.0
//
// mkit external-signer protocol v1.
//
// The protocol mkit v0.1.0 speaks to subprocess signers. v1 is the
// only protocol; there is no fallback. A future breaking revision
// will introduce a sibling `signer2.proto` rather than mutating the
// v1 schema in place.
//
// ## Wire framing
//
// Every message on the wire is a length-prefixed `SignerFrame`:
//
// [u32 LE length][N bytes protobuf-encoded SignerFrame]
//
// The 4-byte length prefix is the encoded length of the SignerFrame in
// bytes (NOT including the prefix itself). It is bounded by
// `MAX_FRAME_BYTES = 1 MiB`. Larger payloads are rejected with
// `ERROR_CODE_INVALID_REQUEST` and the connection closed.
//
// ## Transport
//
// Reference signers run as subprocess; mkit drives the protocol over
// stdin/stdout. Other deployments (Unix socket, vsock) MAY use the
// same framing.
//
// ## Conversation flow
//
// mkit -> signer: Hello{ protocol = PROTOCOL_VERSION_1, want_capabilities }
// signer -> mkit: HelloResponse{ protocol, capabilities }
// mkit -> signer: SignRequest{ algorithm, key_form, key_ref, payload }
// signer -> mkit: SignResponse{ signature, public_key, key_id }
// OR
// signer -> mkit: Error{ code, message, details }
// OR (if authentication required mid-sign)
// signer -> mkit: PinPrompt{ reason, retries_remaining }
// mkit -> signer: PinResponse{ pin }
// signer -> mkit: SignResponse OR Error
//
// `Hello` MUST be the first frame sent by the caller. The signer MAY
// emit `Error` at any point. Either side MAY close the connection
// after a terminal response.
edition = "2023";
package mkit.rpc.v1.signer;
import "mkit/rpc/v1/common.proto";
// ----------------------------------------------------------------------------
// SignerFrame — the union of every message on the wire. Receivers
// dispatch on `body`.
message SignerFrame {
oneof body {
Hello hello = 1;
HelloResponse hello_response = 2;
SignRequest sign_request = 3;
SignResponse sign_response = 4;
PinPrompt pin_prompt = 5;
PinResponse pin_response = 6;
Error error = 7;
}
}
// ----------------------------------------------------------------------------
// Hello + capability discovery.
message Hello {
ProtocolVersion protocol = 1;
// Free-text caller identification, e.g. "mkit/0.1.0". Advisory; the
// signer MUST NOT make security decisions based on this string.
string caller_id = 2;
// If true, the signer SHOULD respond with HelloResponse populated
// and pause for a SignRequest. If false, the signer MAY skip the
// capabilities payload (used in fast paths where mkit already
// knows the signer's capabilities).
bool want_capabilities = 3;
}
message HelloResponse {
ProtocolVersion protocol = 1;
// Free-text signer identification, e.g. "mkit-sign-file/0.1.0".
// Advisory.
string signer_id = 2;
Capabilities capabilities = 3;
}
message Capabilities {
// Algorithms this signer can produce signatures with.
repeated Algorithm algorithms = 1;
// Key forms this signer accepts.
repeated KeyForm key_forms = 2;
// True if the signer supports a PIN prompt round-trip mid-sign.
// Hardware signers (CTAP, TPM with auth value) typically set this.
bool supports_pin = 3;
// True if the signer can return a certificate chain alongside the
// signature (e.g. a TPM attestation certificate, a WebAuthn
// attestation chain, an SE manufacturer cert).
bool supports_certificate_chain = 4;
// Maximum payload size in bytes the signer will accept in a single
// SignRequest. Zero means "use the framing default" (1 MiB).
uint32 max_payload_bytes = 5;
// True if the signer requires user presence (touch / PIN / biometric)
// before it will sign. Advisory: callers MAY surface a UI hint.
bool requires_user_presence = 6;
}
// ----------------------------------------------------------------------------
// Sign request / response.
message SignRequest {
Algorithm algorithm = 1;
KeyForm key_form = 2;
// Identifies the key to sign with. Interpretation depends on
// key_form:
// - KEY_FORM_RAW_BYTES / KEY_FORM_PKCS8_DER: empty (the signer
// loads the key from its own configured path). A future
// extension MAY allow inline key bytes; the file signer MUST
// refuse them today.
// - KEY_FORM_OPAQUE_HANDLE: opaque bytes interpreted by the
// signer (TPM handle, CTAP credentialId, SE label).
bytes key_ref = 3;
// The bytes to sign. For DSSE-flavoured signing this is the PAE
// (RFC 5234-style length-prefixed pre-authentication encoding) per
// SPEC-ATTESTATIONS §4. The signer treats it as opaque bytes.
bytes payload = 4;
// Optional context bytes (e.g. WebAuthn clientDataJSON for the
// ED25519_WEBAUTHN algorithm). Advisory: signers MAY refuse if
// they cannot honour it.
bytes context = 5;
}
message SignResponse {
bytes signature = 1;
// Public key bytes in the natural form for the algorithm:
// - ED25519: 32 bytes
// - SECP256K1: 33 bytes (compressed) or 65 bytes (uncompressed)
// - P256: 33 bytes (compressed) or 65 bytes (uncompressed)
// - ED25519_WEBAUTHN: 32 bytes (the underlying Ed25519 public key)
bytes public_key = 2;
Algorithm algorithm = 3;
// Stable identifier for the signing key, used in DSSE
// `signatures[].keyid`. The convention mkit's built-in signers use:
//
// - Ed25519 (incl. ED25519_WEBAUTHN): "blake3:<64-hex>" where the
// digest is BLAKE3-256 over the raw 32-byte public key (matches
// `mkit-attest/src/signer_repo_key.rs`).
// - secp256k1: "secp256k1:<hex of compressed pubkey>".
// - P-256: "p256:<hex of compressed pubkey>".
//
// Third-party signers MAY return any identifier scheme; mkit treats
// it as opaque on the consumer side.
string key_id = 4;
// Optional certificate chain (DER-encoded X.509). Empty for raw-key
// signers; populated for hardware signers that produce attestable
// signatures.
repeated bytes certificate_chain = 5;
// CTAP / WebAuthn signers wrap the signature in a WebAuthn assertion
// (auth_data || SHA-256(clientDataJSON) is what the authenticator
// signs, NOT the raw payload). The verifier needs both pieces to
// reconstruct the signed input. Unset for non-WebAuthn signers.
WebAuthnData webauthn = 6;
}
// Extension carrying the WebAuthn assertion bits the verifier needs.
// Only populated by CTAP-flavoured signers when `algorithm =
// ALGORITHM_ED25519_WEBAUTHN` or `algorithm = ALGORITHM_P256` *and*
// the underlying signer is FIDO2/WebAuthn (advertised via the
// signer_id string). Verifiers reconstruct the signed input as
// `authenticator_data || SHA-256(client_data_json)` and check the
// `signature` field of `SignResponse` against that.
message WebAuthnData {
// Raw `authenticatorData` bytes per WebAuthn §6.1. Includes the
// RP ID hash, flags, signature counter, and (during enrolment)
// the attested credential data.
bytes authenticator_data = 1;
// Raw UTF-8 `clientDataJSON` per WebAuthn §5.8.1. The verifier
// re-derives this from the request payload and compares
// byte-for-byte to bind the assertion to the request.
bytes client_data_json = 2;
}
// ----------------------------------------------------------------------------
// PIN / auth flow.
message PinPrompt {
// Human-readable reason for the prompt. The caller MAY display it.
// Length-capped at 256 bytes.
string reason = 1;
// Retries remaining before the key locks (CTAP / TPM / PKCS#11
// authValue counters). Zero means "do not know" — NOT zero retries
// remaining.
uint32 retries_remaining = 2;
// True if the signer is asking for a PIN; false if it is asking
// for a touch / biometric (in which case `pin` in PinResponse is
// ignored).
bool wants_pin = 3;
}
message PinResponse {
// PIN bytes. UTF-8. The caller is responsible for not echoing this
// to logs. The signer MUST zeroize after consumption.
// `debug_redact` makes the generated Debug impls print [REDACTED]
// in place of the value, so a stray `{:?}` log cannot leak it.
string pin = 1 [debug_redact = true];
}