1use serde_json::{json, Value};
9
10use crate::{
11 authorization::HolderAuthorizationStatement, Credential, Issuer, IssuerId, ProtocolV1,
12 Revocation,
13};
14
15fn canonicalize_json_value(value: &Value) -> serde_json::Result<Vec<u8>> {
25 serde_json_canonicalizer::to_vec(value)
26}
27
28pub const PBRSA_PUBLIC_INFO_CANONICAL_TYPE: &str = "fedibtc.credentials.public-info";
30
31pub const PBRSA_BLIND_MSG_CANONICAL_TYPE: &str = "fedibtc.credentials.blind-msg";
33
34pub const ISSUER_AUTHORITY_CANONICAL_TYPE: &str = "fedibtc.credentials.issuer-authority";
36
37pub const REVOCATION_CANONICAL_TYPE: &str = "fedibtc.credentials.revocation";
39
40pub const HOLDER_AUTHORIZATION_CANONICAL_TYPE: &str = "fedibtc.credentials.holder-authorization";
42
43pub fn canonicalize_pbrsa_info(
48 version: ProtocolV1,
49 issuer_id: &IssuerId,
50 info: &Value,
51) -> serde_json::Result<Vec<u8>> {
52 let payload = json!({
53 "type": PBRSA_PUBLIC_INFO_CANONICAL_TYPE,
54 "version": version,
55 "issuer_id_pubkey": issuer_id,
56 "info": info,
57 });
58
59 canonicalize_json_value(&payload)
60}
61
62pub fn canonicalize_pbrsa_blind_msg(
67 version: ProtocolV1,
68 blind_msg: &Value,
69) -> serde_json::Result<Vec<u8>> {
70 let payload = json!({
71 "type": PBRSA_BLIND_MSG_CANONICAL_TYPE,
72 "version": version,
73 "blind_msg": blind_msg,
74 });
75
76 canonicalize_json_value(&payload)
77}
78
79pub fn canonicalize_issuer_authority(issuer: &Issuer) -> serde_json::Result<Vec<u8>> {
81 let payload = json!({
82 "type": ISSUER_AUTHORITY_CANONICAL_TYPE,
83 "issuer": issuer,
84 });
85
86 canonicalize_json_value(&payload)
87}
88
89pub fn canonicalize_revocation(revocation: &Revocation) -> serde_json::Result<Vec<u8>> {
91 let payload = json!({
92 "type": REVOCATION_CANONICAL_TYPE,
93 "revocation": revocation,
94 });
95
96 canonicalize_json_value(&payload)
97}
98
99pub fn canonicalize_holder_authorization(
101 authorization: &HolderAuthorizationStatement,
102) -> serde_json::Result<Vec<u8>> {
103 let payload = json!({
104 "type": HOLDER_AUTHORIZATION_CANONICAL_TYPE,
105 "version": ProtocolV1,
106 "authorization": authorization,
107 });
108
109 canonicalize_json_value(&payload)
110}
111
112pub fn canonicalize_credential(credential: &Credential) -> serde_json::Result<Vec<u8>> {
114 let value = serde_json::to_value(credential)?;
115 canonicalize_json_value(&value)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use serde_json::json;
122
123 use crate::{
124 CredentialDigest, HolderAuthorizationStatement, HolderId, IssuerId, ProtocolV1,
125 SubjectPubkey, Timestamp,
126 };
127
128 #[test]
129 fn canonicalizes_object_keys_recursively() {
130 let value = json!({
131 "z": 1,
132 "a": {
133 "b": true,
134 "a": false,
135 },
136 "m": [ { "y": null, "x": "value" } ],
137 });
138
139 assert_eq!(
140 canonicalize_json_value(&value).unwrap(),
141 br#"{"a":{"a":false,"b":true},"m":[{"x":"value","y":null}],"z":1}"#
142 );
143 }
144
145 #[test]
146 fn canonicalizes_numbers_without_insignificant_syntax() {
147 let value: Value =
148 serde_json::from_str(r#"{"b": false, "c": 12e1, "a": "Hello!"}"#).expect("valid json");
149
150 assert_eq!(
151 canonicalize_json_value(&value).unwrap(),
152 br#"{"a":"Hello!","b":false,"c":120}"#
153 );
154 }
155
156 #[test]
157 fn timestamp_serializes_as_json_number() {
158 assert_eq!(serde_json::to_value(Timestamp(1_000)).unwrap(), json!(1000));
159
160 let timestamp: Timestamp = serde_json::from_value(json!(2_000)).unwrap();
161 assert_eq!(timestamp, Timestamp(2_000));
162 }
163
164 #[test]
165 fn public_info_payload_is_jcs_canonical() {
166 let issuer_id = IssuerId(nostr::PublicKey::from_byte_array([1u8; 32]));
167 let info = json!({
168 "z": 1,
169 "a": {
170 "b": true,
171 "a": false,
172 },
173 });
174
175 let canonicalized = canonicalize_pbrsa_info(ProtocolV1, &issuer_id, &info).unwrap();
176 let expected = format!(
177 r#"{{"info":{{"a":{{"a":false,"b":true}},"z":1}},"issuer_id_pubkey":"{}","type":"{}","version":1}}"#,
178 issuer_id.0, PBRSA_PUBLIC_INFO_CANONICAL_TYPE,
179 );
180
181 assert_eq!(canonicalized, expected.as_bytes());
182 }
183
184 #[test]
185 fn blind_msg_payload_is_jcs_canonical() {
186 let blind_msg = json!({
187 "holder": "alice",
188 "nonce": 7,
189 });
190
191 assert_eq!(
192 canonicalize_pbrsa_blind_msg(ProtocolV1, &blind_msg).unwrap(),
193 format!(
194 r#"{{"blind_msg":{{"holder":"alice","nonce":7}},"type":"{}","version":1}}"#,
195 PBRSA_BLIND_MSG_CANONICAL_TYPE,
196 )
197 .as_bytes()
198 );
199 }
200
201 #[test]
202 fn holder_authorization_payload_is_jcs_canonical() {
203 let holder_id = HolderId(nostr::PublicKey::from_byte_array([4u8; 32]));
204 let subject_pubkey = SubjectPubkey(nostr::PublicKey::from_byte_array([2u8; 32]));
205 let authorization = HolderAuthorizationStatement {
206 holder_id_pubkey: holder_id.clone(),
207 subject_pubkey: subject_pubkey.clone(),
208 credential_digest: CredentialDigest([3u8; 32].into()),
209 issued_at: Timestamp(1000),
210 };
211
212 let canonicalized = canonicalize_holder_authorization(&authorization).unwrap();
213 let expected = format!(
214 r#"{{"authorization":{{"credential_digest":"AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM","holder_id_pubkey":"{}","issued_at":1000,"subject_pubkey":"{}"}},"type":"{}","version":1}}"#,
215 holder_id.0, subject_pubkey.0, HOLDER_AUTHORIZATION_CANONICAL_TYPE,
216 );
217
218 assert_eq!(canonicalized, expected.as_bytes());
219 }
220}