1use crate::{artifacts::SigningKeyExt, identity::Attribute};
4use alloc::string::String;
5use alloc::vec::Vec;
6use irma::{ProofStatus, SessionStatus};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Parameters<T> {
13 pub format_version: u8,
15
16 pub public_key: T,
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
32#[serde(deny_unknown_fields)]
33pub struct DisclosureAttribute {
34 #[serde(rename = "t")]
36 pub atype: String,
37
38 #[serde(rename = "v")]
40 pub value: Option<String>,
41
42 #[serde(default, skip_serializing_if = "crate::util::is_false")]
44 pub optional: bool,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct IrmaAuthRequest {
56 pub con: Vec<ConItem>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub validity: Option<u64>,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct KeyResponse<T> {
67 pub status: SessionStatus,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub proof_status: Option<ProofStatus>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub key: Option<T>,
77}
78
79#[derive(Debug, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82pub struct SigningKeyRequest {
83 pub pub_sign_id: Vec<Attribute>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub priv_sign_id: Option<Vec<Attribute>>,
89}
90
91#[derive(Debug, Serialize, Clone)]
100#[serde(untagged)]
101pub enum ConItem {
102 Single(DisclosureAttribute),
104 Discon(Vec<Vec<DisclosureAttribute>>),
106}
107
108impl<'de> Deserialize<'de> for ConItem {
115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116 where
117 D: serde::Deserializer<'de>,
118 {
119 struct ConItemVisitor;
120
121 impl<'de> serde::de::Visitor<'de> for ConItemVisitor {
122 type Value = ConItem;
123
124 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125 f.write_str(
126 "an attribute object like {\"t\": …} or a disjunction \
127 (array of conjunctions, i.e. array of arrays of attributes)",
128 )
129 }
130
131 fn visit_map<A>(self, map: A) -> Result<ConItem, A::Error>
132 where
133 A: serde::de::MapAccess<'de>,
134 {
135 DisclosureAttribute::deserialize(serde::de::value::MapAccessDeserializer::new(map))
136 .map(ConItem::Single)
137 }
138
139 fn visit_seq<A>(self, seq: A) -> Result<ConItem, A::Error>
140 where
141 A: serde::de::SeqAccess<'de>,
142 {
143 Vec::<Vec<DisclosureAttribute>>::deserialize(
144 serde::de::value::SeqAccessDeserializer::new(seq),
145 )
146 .map(ConItem::Discon)
147 }
148 }
149
150 deserializer.deserialize_any(ConItemVisitor)
151 }
152}
153
154#[derive(Debug, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct SigningKeyResponse {
158 pub status: SessionStatus,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub proof_status: Option<ProofStatus>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
168 pub pub_sign_key: Option<SigningKeyExt>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub priv_sign_key: Option<SigningKeyExt>,
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
182 fn irma_auth_request_accepts_discon_entry() {
183 let body = r#"{
184 "con": [
185 { "t": "pbdf.sidn-pbdf.email.email" },
186 [
187 [ { "t": "pbdf.gemeente.personalData.fullname" } ],
188 [
189 { "t": "pbdf.pbdf.passport.firstName" },
190 { "t": "pbdf.pbdf.passport.lastName" }
191 ]
192 ]
193 ]
194 }"#;
195
196 let req: IrmaAuthRequest =
197 serde_json::from_str(body).expect("body should parse with a discon entry");
198
199 assert_eq!(req.con.len(), 2);
200 match &req.con[0] {
201 ConItem::Single(a) => assert_eq!(a.atype, "pbdf.sidn-pbdf.email.email"),
202 other => panic!("expected Single, got {:?}", other),
203 }
204 match &req.con[1] {
205 ConItem::Discon(d) => {
206 assert_eq!(d.len(), 2, "two alternatives");
207 assert_eq!(d[0].len(), 1, "first alt: one attr");
208 assert_eq!(d[1].len(), 2, "second alt: firstName+lastName");
209 assert_eq!(d[0][0].atype, "pbdf.gemeente.personalData.fullname");
210 assert_eq!(d[1][0].atype, "pbdf.pbdf.passport.firstName");
211 assert_eq!(d[1][1].atype, "pbdf.pbdf.passport.lastName");
212 }
213 other => panic!("expected Discon, got {:?}", other),
214 }
215 }
216
217 #[test]
222 fn irma_auth_request_rejects_unknown_attribute_field() {
223 let body =
224 r#"{ "con": [ { "t": "pbdf.sidn-pbdf.email.email", "vaule": "alice@example.com" } ] }"#;
225
226 let err = serde_json::from_str::<IrmaAuthRequest>(body)
227 .expect_err("a misspelled attribute field must be rejected");
228 let msg = alloc::string::ToString::to_string(&err);
229 assert!(
230 msg.contains("vaule"),
231 "error must name the unknown field, got: {msg}"
232 );
233 }
234
235 #[test]
237 fn irma_auth_request_rejects_unknown_top_level_field() {
238 let body = r#"{ "con": [ { "t": "pbdf.sidn-pbdf.email.email" } ], "validty": 300 }"#;
239
240 let err = serde_json::from_str::<IrmaAuthRequest>(body)
241 .expect_err("a misspelled top-level field must be rejected");
242 let msg = alloc::string::ToString::to_string(&err);
243 assert!(
244 msg.contains("validty"),
245 "error must name the unknown field, got: {msg}"
246 );
247 }
248
249 #[test]
253 fn con_item_nesting_mistake_gets_a_useful_error() {
254 let body = r#"{ "con": [ [ { "t": "pbdf.gemeente.personalData.fullname" } ] ] }"#;
255
256 let err = serde_json::from_str::<IrmaAuthRequest>(body)
257 .expect_err("attributes directly inside a disjunction must be rejected");
258 let msg = alloc::string::ToString::to_string(&err);
259 assert!(
260 !msg.contains("did not match any variant"),
261 "must not surface the untagged-enum catch-all, got: {msg}"
262 );
263 }
264
265 #[test]
268 fn irma_auth_request_keeps_parsing_flat_con() {
269 let body = r#"{
270 "con": [
271 { "t": "pbdf.sidn-pbdf.email.email" },
272 { "t": "pbdf.gemeente.personalData.fullname", "optional": true }
273 ]
274 }"#;
275
276 let req: IrmaAuthRequest = serde_json::from_str(body).expect("legacy flat con must parse");
277
278 assert_eq!(req.con.len(), 2);
279 for item in &req.con {
280 assert!(matches!(item, ConItem::Single(_)), "all entries Single");
281 }
282 if let ConItem::Single(a) = &req.con[1] {
283 assert!(a.optional, "optional flag preserved");
284 }
285 }
286}