Skip to main content

auths_pairing_protocol/
types.rs

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