credential_exchange_format/
document.rs

1//! # Document Credentials
2
3use serde::{Deserialize, Serialize};
4
5use crate::{B64Url, EditableField, EditableFieldString, EditableFieldValue, Extension};
6
7#[derive(Clone, Debug, Default, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase", bound(deserialize = "E: Deserialize<'de>"))]
9pub struct CustomFieldsCredential<E = ()> {
10    /// A unique identifier for the CustomFields. It MUST be a machine-generated opaque byte
11    /// sequence with a maximum size of 64 bytes. It SHOULD NOT be displayed to the user.
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub id: Option<B64Url>,
14    /// This member is a [human-palatable](https://www.w3.org/TR/webauthn-3/#human-palatability)
15    /// title to describe the section. This value MAY be set by the credential owner.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub label: Option<String>,
18    /// The collection of miscellaneous fields under this section.
19    pub fields: Vec<EditableFieldValue<E>>,
20    /// This member permits the exporting provider to add additional information associated to this
21    /// CustomFields. This MAY be used to provide an exchange where a minimal amount of information
22    /// is lost.
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub extensions: Vec<Extension<E>>,
25}
26
27/// A [FileCredential] acts as a placeholder to an arbitrary binary file holding its associated
28/// metadata. When an importing provider encounters a file credential, they MAY request the file
29/// afterwards if they have a direct exchange. If the exchange will produce an export response file,
30/// then the associated encrypted file MUST be stored in the documents folder of the zip archive.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct FileCredential {
34    /// The file’s identifier, used as the file name in the zip archive.
35    pub id: B64Url,
36    /// The file name with the file extension if applicable.
37    pub name: String,
38    /// The file’s decrypted size in bytes.
39    pub decrypted_size: u64,
40    /// The SHA256 hash of the decrypted file. This hash MUST be used by the importing provider
41    /// when the file is decrypted to ensure that it has not been corrupted.
42    pub integrity_hash: B64Url,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase", bound(deserialize = "E: Deserialize<'de>"))]
47pub struct NoteCredential<E = ()> {
48    /// This member is a user-defined value encoded as a UTF-8 string.
49    pub content: EditableField<EditableFieldString, E>,
50}
51
52#[cfg(test)]
53mod tests {
54    use chrono::Month;
55    use serde_json::json;
56
57    use super::*;
58    use crate::{EditableFieldBoolean, EditableFieldString, EditableFieldYearMonth};
59
60    #[test]
61    fn test_serialize_custom_fields() {
62        let credential = CustomFieldsCredential {
63            id: None,
64            label: None,
65            fields: vec![
66                EditableFieldValue::<()>::String(EditableField {
67                    id: Some(B64Url::from(b"field1".as_slice())),
68                    value: EditableFieldString("hello".into()),
69                    label: None,
70                    extensions: None,
71                }),
72                EditableFieldValue::<()>::Boolean(EditableField {
73                    id: None,
74                    value: EditableFieldBoolean(false),
75                    label: None,
76                    extensions: None,
77                }),
78            ],
79            extensions: vec![],
80        };
81
82        let json = json!({
83            "fields": [
84                {
85                    "id": "ZmllbGQx",
86                    "fieldType": "string",
87                    "value": "hello"
88                },
89                {
90                    "fieldType": "boolean",
91                    "value": "false"
92                }
93            ]
94        });
95
96        assert_eq!(serde_json::to_value(&credential).unwrap(), json);
97    }
98
99    #[test]
100    fn test_deserialize_custom_fields() {
101        let json = json!({
102            "fields": [
103                {
104                    "fieldType": "string",
105                    "value": "hello"
106                },
107                {
108                    "fieldType": "concealed-string",
109                    "value": "world"
110                },
111                {
112                    "fieldType": "boolean",
113                    "value": "false"
114                },
115                {
116                    "fieldType": "year-month",
117                    "value": "2025-02"
118                }
119            ]
120        });
121
122        let json = serde_json::to_string(&json).unwrap();
123        let credential: CustomFieldsCredential = serde_json::from_str(&json).unwrap();
124
125        assert_eq!(credential.id, None);
126        assert_eq!(credential.label, None);
127        assert_eq!(credential.extensions.len(), 0);
128        assert_eq!(credential.fields.len(), 4);
129
130        match &credential.fields[0] {
131            EditableFieldValue::String(field) => {
132                assert_eq!(field.value.0, "hello");
133            }
134            _ => panic!("Expected string field"),
135        }
136
137        match &credential.fields[1] {
138            EditableFieldValue::ConcealedString(field) => {
139                assert_eq!(field.value.0, "world");
140            }
141            _ => panic!("Expected concealed string field"),
142        }
143
144        match &credential.fields[2] {
145            EditableFieldValue::Boolean(field) => {
146                assert!(!field.value.0);
147            }
148            _ => panic!("Expected boolean field"),
149        }
150
151        match &credential.fields[3] {
152            EditableFieldValue::YearMonth(field) => {
153                assert_eq!(
154                    field.value,
155                    EditableFieldYearMonth {
156                        year: 2025,
157                        month: Month::February,
158                    }
159                );
160            }
161            _ => panic!("Expected boolean field"),
162        }
163    }
164}