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];
}

Files