Skip to main content

auc_tool/application/
protocol.rs

1use serde::{Deserialize, Serialize};
2
3pub const PROTOCOL_MAJOR: u16 = 1;
4pub(crate) const MAX_FRAME_BYTES: usize = 64 * 1024;
5
6#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
7pub struct RequestId([u8; 16]);
8
9impl RequestId {
10    pub fn random() -> Self {
11        use rand::RngExt as _;
12
13        Self(rand::rng().random())
14    }
15}
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(rename_all = "kebab-case", tag = "method")]
19pub enum ApplicationRequest {
20    Status,
21    Touch,
22    ListCredentials,
23    DeleteCredential {
24        credential_id: String,
25    },
26    #[serde(other)]
27    Unknown,
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "kebab-case", tag = "result")]
32pub enum ApplicationResponse {
33    Status(Status),
34    Touch(TouchReceipt),
35    Credentials { credentials: Vec<CredentialSummary> },
36    Deleted { credential_id: String },
37}
38
39#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40pub struct Status {
41    pub product: String,
42    pub package: String,
43    pub version: String,
44    pub protocol_major: u16,
45    pub device_present: bool,
46    pub pending_touch: bool,
47    pub credential_count: usize,
48}
49
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct TouchReceipt {
52    pub operation: String,
53    pub rp_id: String,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57pub struct CredentialSummary {
58    pub credential_id: String,
59    pub rp_id: String,
60    pub user_name: Option<String>,
61    pub discoverable: bool,
62    pub backup_eligible: bool,
63    pub backed_up: bool,
64}
65
66#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum ErrorCode {
69    BadRequest,
70    Unauthorized,
71    UnsupportedProtocol,
72    NotFound,
73    Conflict,
74    Unavailable,
75    Internal,
76}
77
78#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
79pub struct ProtocolError {
80    pub code: ErrorCode,
81    pub message: String,
82}
83
84impl ProtocolError {
85    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
86        Self {
87            code,
88            message: message.into(),
89        }
90    }
91}
92
93#[derive(Debug, thiserror::Error)]
94pub enum ApplicationError {
95    #[error("auc application frame exceeds the {MAX_FRAME_BYTES}-byte limit")]
96    FrameTooLarge,
97    #[error("auc agent closed the connection before sending a complete frame")]
98    EarlyEof,
99    #[error("failed to encode auc application CBOR: {0}")]
100    Encode(String),
101    #[error("failed to decode auc application CBOR: {0}")]
102    Decode(String),
103    #[error("auc response request ID did not match the request")]
104    MismatchedRequestId,
105    #[error("auc application protocol v{0} is unsupported")]
106    UnsupportedProtocol(u16),
107    #[error("auc request failed ({code:?}): {message}")]
108    Remote { code: ErrorCode, message: String },
109    #[error("auc application I/O failed: {0}")]
110    Io(#[from] std::io::Error),
111}
112
113#[derive(Debug, Deserialize, Serialize)]
114pub(crate) struct RequestEnvelope {
115    pub request_id: RequestId,
116    pub minimum_protocol_major: u16,
117    pub maximum_protocol_major: u16,
118    pub request: ApplicationRequest,
119}
120
121#[derive(Debug, Deserialize, Serialize)]
122pub(crate) struct ResponseEnvelope {
123    pub request_id: RequestId,
124    pub protocol_major: u16,
125    pub body: ResponseBody,
126}
127
128#[derive(Debug, Deserialize, Serialize)]
129#[serde(rename_all = "kebab-case", tag = "status", content = "body")]
130pub(crate) enum ResponseBody {
131    Ok(ApplicationResponse),
132    Error(ProtocolError),
133}
134
135pub(crate) fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, ApplicationError> {
136    let mut bytes = Vec::new();
137    ciborium::into_writer(value, &mut bytes)
138        .map_err(|error| ApplicationError::Encode(error.to_string()))?;
139    if bytes.len() > MAX_FRAME_BYTES {
140        return Err(ApplicationError::FrameTooLarge);
141    }
142    Ok(bytes)
143}
144
145pub(crate) fn decode<T>(bytes: &[u8]) -> Result<T, ApplicationError>
146where
147    T: for<'de> Deserialize<'de>,
148{
149    if bytes.len() > MAX_FRAME_BYTES {
150        return Err(ApplicationError::FrameTooLarge);
151    }
152    ciborium::from_reader(bytes).map_err(|error| ApplicationError::Decode(error.to_string()))
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn requests_round_trip_and_frames_are_bounded() {
161        let envelope = RequestEnvelope {
162            request_id: RequestId([0xDE; 16]),
163            minimum_protocol_major: 1,
164            maximum_protocol_major: 1,
165            request: ApplicationRequest::DeleteCredential {
166                credential_id: "deadbeef".to_string(),
167            },
168        };
169        let decoded: RequestEnvelope = decode(&encode(&envelope).unwrap()).unwrap();
170        assert_eq!(decoded.request_id, envelope.request_id);
171        assert_eq!(decoded.request, envelope.request);
172        assert!(matches!(
173            decode::<RequestEnvelope>(&vec![0; MAX_FRAME_BYTES + 1]),
174            Err(ApplicationError::FrameTooLarge)
175        ));
176    }
177
178    #[test]
179    fn protocol_v1_ignores_additive_fields() {
180        let envelope = RequestEnvelope {
181            request_id: RequestId([0xDE; 16]),
182            minimum_protocol_major: PROTOCOL_MAJOR,
183            maximum_protocol_major: PROTOCOL_MAJOR,
184            request: ApplicationRequest::DeleteCredential {
185                credential_id: "deadbeef".to_string(),
186            },
187        };
188        let mut value: ciborium::Value =
189            ciborium::from_reader(encode(&envelope).unwrap().as_slice()).unwrap();
190        let ciborium::Value::Map(fields) = &mut value else {
191            panic!("application envelope must encode as a map");
192        };
193        fields.push((
194            ciborium::Value::Text("future-envelope-field".to_string()),
195            ciborium::Value::Bool(true),
196        ));
197        let request = fields
198            .iter_mut()
199            .find_map(|(key, value)| {
200                (key == &ciborium::Value::Text("request".to_string())).then_some(value)
201            })
202            .unwrap();
203        let ciborium::Value::Map(request_fields) = request else {
204            panic!("application method must encode as a map");
205        };
206        request_fields.push((
207            ciborium::Value::Text("future-method-field".to_string()),
208            ciborium::Value::Integer(0xDEADBEEF_u64.into()),
209        ));
210        let mut encoded = Vec::new();
211        ciborium::into_writer(&value, &mut encoded).unwrap();
212
213        let decoded: RequestEnvelope = decode(&encoded).unwrap();
214        assert_eq!(decoded.request, envelope.request);
215    }
216}