use crate::{artifacts::SigningKeyExt, identity::Attribute};
use alloc::string::String;
use alloc::vec::Vec;
use irma::{ProofStatus, SessionStatus};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Parameters<T> {
pub format_version: u8,
pub public_key: T,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct DisclosureAttribute {
#[serde(rename = "t")]
pub atype: String,
#[serde(rename = "v")]
pub value: Option<String>,
#[serde(default, skip_serializing_if = "crate::util::is_false")]
pub optional: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IrmaAuthRequest {
pub con: Vec<ConItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validity: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KeyResponse<T> {
pub status: SessionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof_status: Option<ProofStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<T>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SigningKeyRequest {
pub pub_sign_id: Vec<Attribute>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priv_sign_id: Option<Vec<Attribute>>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(untagged)]
pub enum ConItem {
Single(DisclosureAttribute),
Discon(Vec<Vec<DisclosureAttribute>>),
}
impl<'de> Deserialize<'de> for ConItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ConItemVisitor;
impl<'de> serde::de::Visitor<'de> for ConItemVisitor {
type Value = ConItem;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(
"an attribute object like {\"t\": …} or a disjunction \
(array of conjunctions, i.e. array of arrays of attributes)",
)
}
fn visit_map<A>(self, map: A) -> Result<ConItem, A::Error>
where
A: serde::de::MapAccess<'de>,
{
DisclosureAttribute::deserialize(serde::de::value::MapAccessDeserializer::new(map))
.map(ConItem::Single)
}
fn visit_seq<A>(self, seq: A) -> Result<ConItem, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
Vec::<Vec<DisclosureAttribute>>::deserialize(
serde::de::value::SeqAccessDeserializer::new(seq),
)
.map(ConItem::Discon)
}
}
deserializer.deserialize_any(ConItemVisitor)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SigningKeyResponse {
pub status: SessionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof_status: Option<ProofStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pub_sign_key: Option<SigningKeyExt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priv_sign_key: Option<SigningKeyExt>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn irma_auth_request_accepts_discon_entry() {
let body = r#"{
"con": [
{ "t": "pbdf.sidn-pbdf.email.email" },
[
[ { "t": "pbdf.gemeente.personalData.fullname" } ],
[
{ "t": "pbdf.pbdf.passport.firstName" },
{ "t": "pbdf.pbdf.passport.lastName" }
]
]
]
}"#;
let req: IrmaAuthRequest =
serde_json::from_str(body).expect("body should parse with a discon entry");
assert_eq!(req.con.len(), 2);
match &req.con[0] {
ConItem::Single(a) => assert_eq!(a.atype, "pbdf.sidn-pbdf.email.email"),
other => panic!("expected Single, got {:?}", other),
}
match &req.con[1] {
ConItem::Discon(d) => {
assert_eq!(d.len(), 2, "two alternatives");
assert_eq!(d[0].len(), 1, "first alt: one attr");
assert_eq!(d[1].len(), 2, "second alt: firstName+lastName");
assert_eq!(d[0][0].atype, "pbdf.gemeente.personalData.fullname");
assert_eq!(d[1][0].atype, "pbdf.pbdf.passport.firstName");
assert_eq!(d[1][1].atype, "pbdf.pbdf.passport.lastName");
}
other => panic!("expected Discon, got {:?}", other),
}
}
#[test]
fn irma_auth_request_rejects_unknown_attribute_field() {
let body =
r#"{ "con": [ { "t": "pbdf.sidn-pbdf.email.email", "vaule": "alice@example.com" } ] }"#;
let err = serde_json::from_str::<IrmaAuthRequest>(body)
.expect_err("a misspelled attribute field must be rejected");
let msg = alloc::string::ToString::to_string(&err);
assert!(
msg.contains("vaule"),
"error must name the unknown field, got: {msg}"
);
}
#[test]
fn irma_auth_request_rejects_unknown_top_level_field() {
let body = r#"{ "con": [ { "t": "pbdf.sidn-pbdf.email.email" } ], "validty": 300 }"#;
let err = serde_json::from_str::<IrmaAuthRequest>(body)
.expect_err("a misspelled top-level field must be rejected");
let msg = alloc::string::ToString::to_string(&err);
assert!(
msg.contains("validty"),
"error must name the unknown field, got: {msg}"
);
}
#[test]
fn con_item_nesting_mistake_gets_a_useful_error() {
let body = r#"{ "con": [ [ { "t": "pbdf.gemeente.personalData.fullname" } ] ] }"#;
let err = serde_json::from_str::<IrmaAuthRequest>(body)
.expect_err("attributes directly inside a disjunction must be rejected");
let msg = alloc::string::ToString::to_string(&err);
assert!(
!msg.contains("did not match any variant"),
"must not surface the untagged-enum catch-all, got: {msg}"
);
}
#[test]
fn irma_auth_request_keeps_parsing_flat_con() {
let body = r#"{
"con": [
{ "t": "pbdf.sidn-pbdf.email.email" },
{ "t": "pbdf.gemeente.personalData.fullname", "optional": true }
]
}"#;
let req: IrmaAuthRequest = serde_json::from_str(body).expect("legacy flat con must parse");
assert_eq!(req.con.len(), 2);
for item in &req.con {
assert!(matches!(item, ConItem::Single(_)), "all entries Single");
}
if let ConItem::Single(a) = &req.con[1] {
assert!(a.optional, "optional flag preserved");
}
}
}