Skip to main content

auths_pairing_protocol/
types.rs

1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
2use serde::{Deserialize, Serialize};
3
4/// A base64url-encoded (no padding) byte string.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[serde(transparent)]
8pub struct Base64UrlEncoded(String);
9
10impl Base64UrlEncoded {
11    pub fn from_raw(s: String) -> Self {
12        Self(s)
13    }
14
15    pub fn encode(bytes: &[u8]) -> Self {
16        Self(URL_SAFE_NO_PAD.encode(bytes))
17    }
18
19    pub fn decode(&self) -> Result<Vec<u8>, base64::DecodeError> {
20        URL_SAFE_NO_PAD.decode(&self.0)
21    }
22
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28impl std::ops::Deref for Base64UrlEncoded {
29    type Target = str;
30    fn deref(&self) -> &str {
31        &self.0
32    }
33}
34
35impl std::fmt::Display for Base64UrlEncoded {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.write_str(&self.0)
38    }
39}
40
41/// Session status.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[serde(rename_all = "lowercase")]
45pub enum SessionStatus {
46    Pending,
47    Responded,
48    Confirmed,
49    Aborted,
50    Completed,
51    Cancelled,
52    Expired,
53}
54
55/// Request to create a new pairing session.
56///
57/// Pairing is always "add a controller to the shared identity KEL". Key
58/// rotation is a local operation on a device's own KEL — it does not flow
59/// through pairing sessions at all.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub struct CreateSessionRequest {
63    pub session_id: String,
64    pub controller_did: String,
65    pub ephemeral_pubkey: Base64UrlEncoded,
66    pub short_code: String,
67    #[serde(default)]
68    pub capabilities: Vec<String>,
69    pub expires_at: i64,
70    /// Recovery target — populated only by `auths pair --recover`. The DID
71    /// of the old device being replaced. The surviving controller's
72    /// confirmation triggers a `rot_swap_controller` that drops this DID
73    /// and adds the new initiator in a single rotation event.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub recovery_target: Option<String>,
76}
77
78/// Response to session creation.
79#[derive(Debug, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct CreateSessionResponse {
82    pub session_id: String,
83    pub status: SessionStatus,
84    pub short_code: String,
85    pub uri: String,
86    pub ttl_seconds: u64,
87}
88
89/// Request to submit a pairing response.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
92pub struct SubmitResponseRequest {
93    pub device_ephemeral_pubkey: Base64UrlEncoded,
94    pub device_signing_pubkey: Base64UrlEncoded,
95    /// Signing curve for `device_signing_pubkey` / `signature`. Carried in-band
96    /// per the workspace wire-format curve-tagging rule — verifiers must never
97    /// infer curve from pubkey byte length. Defaults to P-256 when absent.
98    #[serde(default)]
99    pub curve: crate::response::CurveTag,
100    /// Responder's DID string (e.g. `did:key:z6Mk...`).
101    pub device_did: String,
102    pub signature: Base64UrlEncoded,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub device_name: Option<String>,
105    /// Optional per-pairing subkey chain for controller-correlation
106    /// privacy. When present, `device_signing_pubkey` is a fresh
107    /// session-only subkey and `subkey_chain.bootstrap_pubkey` holds
108    /// the stable phone-level key that signed the subkey binding.
109    /// A daemon built with the `subkey-chain-v1` feature verifies the
110    /// chain and records `bootstrap_pubkey` as the stable phone
111    /// identifier; a daemon built without the feature rejects any
112    /// `Some(_)` with an explicit unsupported-extension error.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub subkey_chain: Option<crate::subkey_chain::SubkeyChain>,
115    /// The initiator's device-KEL inception event, base64url-no-pad JSON.
116    ///
117    /// Both sides validate the other's `icp` before either side signs
118    /// the shared-KEL change. The daemon is an untrusted relay and does
119    /// not inspect this field. `#[serde(default)]` so legacy pair
120    /// responses that pre-date mutual inception exchange continue to
121    /// deserialize (the responder-side code must still enforce presence
122    /// when it expects the inception data).
123    #[serde(default)]
124    pub initiator_inception_event: String,
125    /// The responder's device-KEL inception event, base64url-no-pad JSON.
126    #[serde(default)]
127    pub responder_inception_event: String,
128    /// Shared-KEL inception event — populated by the Mac (initiator)
129    /// when no shared identity KEL exists yet (first-ever pair). The
130    /// responder validates self-consistency + Mac's signature, then
131    /// replicates. Size-capped at 1 KB on serialize to guard against
132    /// future multi-sig KEL inceptions overflowing QR capacity.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub shared_kel_inception_event: Option<String>,
135}
136
137/// Maximum size in bytes for `SubmitResponseRequest::shared_kel_inception_event`.
138///
139/// Single-sig P-256 `icp` events are ~300 bytes base64url-encoded; the cap
140/// reserves headroom while refusing to silently accept multi-sig inceptions
141/// that would overflow QR capacity.
142pub const SHARED_KEL_INCEPTION_EVENT_MAX_BYTES: usize = 1024;
143
144impl SubmitResponseRequest {
145    /// Validate payload-size invariants that must hold before transmission.
146    ///
147    /// Args:
148    /// * `self`: The request to check.
149    ///
150    /// Returns `Err` with a diagnostic string when
151    /// `shared_kel_inception_event` exceeds [`SHARED_KEL_INCEPTION_EVENT_MAX_BYTES`].
152    ///
153    /// Usage:
154    /// ```ignore
155    /// req.validate()?;
156    /// ```
157    pub fn validate(&self) -> Result<(), String> {
158        if let Some(ref inc) = self.shared_kel_inception_event
159            && inc.len() > SHARED_KEL_INCEPTION_EVENT_MAX_BYTES
160        {
161            return Err(format!(
162                "shared_kel_inception_event is {} bytes, exceeds cap of {} — multi-sig KEL inception would overflow QR capacity",
163                inc.len(),
164                SHARED_KEL_INCEPTION_EVENT_MAX_BYTES,
165            ));
166        }
167        Ok(())
168    }
169}
170
171/// Response when getting session status.
172#[derive(Debug, Serialize, Deserialize)]
173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
174pub struct GetSessionResponse {
175    pub session_id: String,
176    pub status: SessionStatus,
177    pub ttl_seconds: u64,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub token: Option<CreateSessionRequest>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub response: Option<SubmitResponseRequest>,
182}
183
184/// Response for successful operations.
185#[derive(Debug, Serialize, Deserialize)]
186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
187pub struct SuccessResponse {
188    pub success: bool,
189    pub message: String,
190}
191
192/// Request to submit a SAS confirmation (or abort).
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
195pub struct SubmitConfirmationRequest {
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub encrypted_attestation: Option<String>,
198    #[serde(default)]
199    pub aborted: bool,
200}
201
202/// Response when polling for confirmation.
203#[derive(Debug, Serialize, Deserialize)]
204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
205pub struct GetConfirmationResponse {
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub encrypted_attestation: Option<String>,
208    #[serde(default)]
209    pub aborted: bool,
210}