kv-proto: OPAQUE suite, RFC 9421 signing profile and M1 auth wire types

Adds the OpaqueSuite (Ristretto255 OPRF, TripleDH, SHA-512, identity KSF)
shared by client and server, the httpsig signature-base builders, and the
setup/credentials/account/device/SSE types for milestone M1.
Cargo.lock changes only for kv-proto's new dependencies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 21:22:03 +09:00
co-authored by Claude Opus 5.5
parent 4a152bde11
commit 48c7e32d33
5 changed files with 355 additions and 0 deletions
+101
View File
@@ -6,6 +6,9 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub mod httpsig;
pub mod opaque;
pub const FORMAT_VERSION: u8 = 1;
pub const API_VERSION: u32 = 1;
@@ -93,11 +96,34 @@ pub struct DeviceEnroll {
pub p256_pk: Vec<u8>,
}
/// `POST /v1/setup/start`: OPAQUE registration, first message. Needs the setup token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupStart {
pub setup_token: String,
pub account_id: Uuid,
/// Serialized opaque-ke `RegistrationRequest`.
#[serde(with = "b64")]
pub registration_request: Vec<u8>,
}
/// Reply to `POST /v1/setup/start` and `POST /v1/account/credentials/start`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationStartResponse {
/// Serialized opaque-ke `RegistrationResponse`.
#[serde(with = "b64")]
pub registration_response: Vec<u8>,
}
/// `POST /v1/setup/finish`. `account_id` must match the one sent to `/v1/setup/start`.
pub type SetupFinish = SetupRequest;
/// Body of `POST /v1/setup/finish` (alias [`SetupFinish`]).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupRequest {
pub setup_token: String,
pub account_id: Uuid,
pub kdf: KdfParams,
/// Serialized opaque-ke `RegistrationUpload`.
#[serde(with = "b64")]
pub opaque_registration_upload: Vec<u8>,
#[serde(with = "b64")]
@@ -108,27 +134,35 @@ pub struct SetupRequest {
pub device: DeviceEnroll,
}
/// `POST /v1/auth/login/start`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginStart {
pub account_id: Uuid,
/// Serialized opaque-ke `CredentialRequest`.
#[serde(with = "b64")]
pub ke1: Vec<u8>,
/// The logging-in device. An unknown `device_id` is enrolled as pending after a successful login.
pub device: DeviceEnroll,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginStartResponse {
/// Handle for `/v1/auth/login/finish`; expires after 60 s.
pub login_id: String,
/// Serialized opaque-ke `CredentialResponse`.
#[serde(with = "b64")]
pub ke2: Vec<u8>,
pub kdf: KdfParams,
}
/// `POST /v1/auth/login/finish`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginFinish {
pub login_id: String,
/// Serialized opaque-ke `CredentialFinalization`.
#[serde(with = "b64")]
pub ke3: Vec<u8>,
/// Reserved for the TOTP enrollment path; not implemented by the server yet.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub totp: Option<String>,
}
@@ -140,13 +174,80 @@ pub enum LoginStatus {
PendingApproval,
}
/// `status: ok` carries a session (put it in `KV-Session`); `pending_approval` carries none:
/// approve the device from an enrolled one, then log in again.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginFinishResponse {
pub status: LoginStatus,
pub session: Option<String>,
/// RFC 3339. Rolling: every authenticated request extends it by 12 h.
pub expires_at: Option<String>,
}
/// `GET /v1/account`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountInfo {
pub account_id: Uuid,
pub kdf: KdfParams,
#[serde(with = "b64")]
pub enc_pk: Vec<u8>,
#[serde(with = "b64")]
pub sig_pk: Vec<u8>,
pub wrapped_keyset: Envelope,
}
/// `POST /v1/account/credentials/start`: OPAQUE re-registration for a password change.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialsStart {
/// Serialized opaque-ke `RegistrationRequest` for the new `AUTH_INPUT`.
#[serde(with = "b64")]
pub registration_request: Vec<u8>,
}
/// `PUT /v1/account/credentials`: replaces the OPAQUE record, KDF params and wrapped keyset atomically.
/// Sessions of all other devices are ended.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialsChange {
pub kdf: KdfParams,
/// Serialized opaque-ke `RegistrationUpload`.
#[serde(with = "b64")]
pub registration_upload: Vec<u8>,
pub wrapped_keyset: Envelope,
}
/// An entry of `GET /v1/devices`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub device_id: Uuid,
pub name: String,
pub platform: Platform,
/// SEC1 uncompressed P-256 public key; show a fingerprint before approving.
#[serde(with = "b64")]
pub p256_pk: Vec<u8>,
pub approved: bool,
pub revoked_at: Option<String>,
pub created_at: String,
/// True for the device that made the request.
pub current: bool,
}
/// Server-Sent Events on `GET /v1/events`. The SSE event name equals `type`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerEvent {
VaultChanged { vault_id: Uuid, head: u64 },
DevicePending { device_id: Uuid },
}
impl ServerEvent {
pub fn name(&self) -> &'static str {
match self {
ServerEvent::VaultChanged { .. } => "vault_changed",
ServerEvent::DevicePending { .. } => "device_pending",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vault {
pub vault_id: Uuid,