1use blind_rsa_signatures::{
2 BlindMessage as PbrsaBlindMessage, BlindSignature as PbrsaBlindSignature,
3 Signature as PbrsaSignature,
4};
5use nostr::secp256k1::{schnorr::Signature, Message};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use serde_json::Value;
8use serde_with::serde_as;
9use sha2::{digest::Output, Digest, Sha256};
10use std::str::FromStr;
11
12use crate::serde::{
13 Base64UrlUnpadded, PbrsaPublicKeyBase64UrlUnpadded, SchnorrSignatureBase64UrlUnpadded,
14 Sha256DigestBase64UrlUnpadded,
15};
16use crate::{
17 canonicalize_credential, canonicalize_issuer_authority, canonicalize_revocation,
18 CredentialsError, PbrsaPublicKey,
19};
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct ProtocolV1;
30
31impl Serialize for ProtocolV1 {
32 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
33 where
34 S: Serializer,
35 {
36 serializer.serialize_u8(1)
37 }
38}
39
40impl<'de> Deserialize<'de> for ProtocolV1 {
41 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42 where
43 D: Deserializer<'de>,
44 {
45 let version = u16::deserialize(deserializer)?;
46 if version == 1 {
47 Ok(Self)
48 } else {
49 Err(serde::de::Error::custom(format_args!(
50 "unsupported protocol version: {version}"
51 )))
52 }
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
60#[serde(transparent)]
61pub struct IssuerId(pub nostr::PublicKey);
62
63impl FromStr for IssuerId {
64 type Err = nostr::key::Error;
65
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 nostr::PublicKey::parse(value).map(Self)
68 }
69}
70
71#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
76#[serde(transparent)]
77pub struct HolderId(pub nostr::PublicKey);
78
79impl FromStr for HolderId {
80 type Err = nostr::key::Error;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
83 nostr::PublicKey::parse(value).map(Self)
84 }
85}
86
87#[derive(
92 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
93)]
94#[serde(transparent)]
95pub struct Timestamp(pub u64);
96
97impl Timestamp {
98 pub const fn as_secs(self) -> u64 {
100 self.0
101 }
102}
103
104impl From<u64> for Timestamp {
105 fn from(value: u64) -> Self {
106 Self(value)
107 }
108}
109
110impl From<Timestamp> for u64 {
111 fn from(value: Timestamp) -> Self {
112 value.0
113 }
114}
115
116#[serde_as]
118#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
119#[serde(transparent)]
120pub struct CredentialDigest(#[serde_as(as = "Sha256DigestBase64UrlUnpadded")] pub Output<Sha256>);
121
122impl From<Output<Sha256>> for CredentialDigest {
123 fn from(value: Output<Sha256>) -> Self {
124 Self(value)
125 }
126}
127
128#[serde_as]
130#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct IssuerSecretKeys {
132 pub issuer_id_secret_key: String,
133 #[serde_as(as = "Base64UrlUnpadded")]
134 pub issuance_secret_key: Vec<u8>,
135}
136
137#[serde_as]
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140pub struct IssuerAuthority {
141 pub version: ProtocolV1,
142 pub issuer: Issuer,
143 pub proof: SchnorrSignatureProof,
144}
145
146#[serde_as]
148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
149pub struct SchnorrSignatureProof {
150 #[serde_as(as = "SchnorrSignatureBase64UrlUnpadded")]
151 pub signature: Signature,
152}
153
154impl IssuerAuthority {
155 pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
157 self.issuer.digest()
158 }
159
160 pub fn verify(&self) -> Result<Issuer, CredentialsError> {
162 validate_revocation_locations(&self.issuer.revocation)?;
163
164 verify_identity_signature(
165 &self.issuer.issuer_id_pubkey,
166 &self.proof.signature,
167 Message::from_digest(self.digest()?.into()),
168 )?;
169
170 Ok(self.issuer.clone())
171 }
172}
173
174#[serde_as]
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177pub struct Issuer {
178 pub issuer_id_pubkey: IssuerId,
179 #[serde_as(as = "PbrsaPublicKeyBase64UrlUnpadded")]
183 pub issuance_key: PbrsaPublicKey,
184 pub revocation: Vec<RevocationLocation>,
185}
186
187impl Issuer {
188 pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
190 let canonical = canonicalize_issuer_authority(self)?;
191 Ok(Sha256::new()
192 .chain_update(ISSUER_AUTHORITY_SIGNATURE_DOMAIN_SEPARATOR)
193 .chain_update(canonical)
194 .finalize())
195 }
196}
197
198#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
200pub struct RevocationLocation {
201 pub protocol: String,
202 pub location: String,
203}
204
205#[serde_as]
207#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub struct SignedRevocation {
209 pub version: ProtocolV1,
210 pub revocation: Revocation,
211 pub proof: RevocationProof,
212}
213
214#[serde_as]
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
217pub struct RevocationProof {
218 pub issuer_id_pubkey: IssuerId,
219 #[serde_as(as = "SchnorrSignatureBase64UrlUnpadded")]
220 pub signature: Signature,
221}
222
223impl SignedRevocation {
224 pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
226 self.revocation.digest()
227 }
228
229 pub fn verify(&self) -> Result<Revocation, CredentialsError> {
231 verify_identity_signature(
232 &self.proof.issuer_id_pubkey,
233 &self.proof.signature,
234 Message::from_digest(self.digest()?.into()),
235 )?;
236
237 Ok(self.revocation.clone())
238 }
239}
240
241#[serde_as]
243#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
244pub struct Revocation {
245 pub credential_digest: CredentialDigest,
247}
248
249impl Revocation {
250 pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
252 let canonical = canonicalize_revocation(self)?;
253 Ok(Sha256::new()
254 .chain_update(REVOCATION_SIGNATURE_DOMAIN_SEPARATOR)
255 .chain_update(canonical)
256 .finalize())
257 }
258}
259
260pub const ISSUER_AUTHORITY_SIGNATURE_DOMAIN_SEPARATOR: &[u8] =
262 b"fedi-credential/issuer-authority-signature/v1\0";
263
264pub const REVOCATION_SIGNATURE_DOMAIN_SEPARATOR: &[u8] =
266 b"fedi-credential/revocation-signature/v1\0";
267
268fn validate_revocation_locations(locations: &[RevocationLocation]) -> Result<(), CredentialsError> {
269 if locations
270 .iter()
271 .any(|location| location.protocol.is_empty() || location.location.is_empty())
272 {
273 return Err(CredentialsError::VerificationFailed);
274 }
275
276 Ok(())
277}
278
279fn verify_identity_signature(
280 issuer_id: &IssuerId,
281 signature: &Signature,
282 message: Message,
283) -> Result<(), CredentialsError> {
284 verify_identity_signature_with_key(&issuer_id.0, signature, message)
285}
286
287pub(crate) fn verify_identity_signature_with_key(
288 identity_public_key: &nostr::PublicKey,
289 signature: &Signature,
290 message: Message,
291) -> Result<(), CredentialsError> {
292 let public_key = identity_public_key
293 .xonly()
294 .map_err(|_| CredentialsError::VerificationFailed)?;
295
296 nostr::SECP256K1
297 .verify_schnorr(signature, &message, &public_key)
298 .map_err(|_| CredentialsError::VerificationFailed)
299}
300
301pub const CREDENTIAL_DIGEST_DOMAIN_SEPARATOR: &[u8] = b"fedi-credential/credential-digest/v1\0";
303
304#[serde_as]
315#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
316pub struct SignedCredential {
317 pub version: ProtocolV1,
318 pub credential: Credential,
319 pub proof: CredentialProof,
320}
321
322#[serde_as]
324#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
325pub struct Credential {
326 pub issuer_id_pubkey: IssuerId,
327 pub info: Value,
328 pub blind_msg: Value,
329}
330
331#[serde_as]
333#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
334pub struct CredentialProof {
335 #[serde_as(as = "Base64UrlUnpadded")]
339 pub signature: PbrsaSignature,
340}
341
342impl Credential {
343 pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
345 let canonical = canonicalize_credential(self)?;
346
347 let mut hasher = Sha256::new();
348 hasher.update(CREDENTIAL_DIGEST_DOMAIN_SEPARATOR);
349 hasher.update(canonical);
350 Ok(hasher.finalize())
351 }
352}
353
354#[serde_as]
359#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
360pub struct IssuanceRequest {
361 pub version: ProtocolV1,
362 #[serde_as(as = "Base64UrlUnpadded")]
366 pub blinded_message: PbrsaBlindMessage,
367}
368
369#[serde_as]
375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
376pub struct IssuanceResponse {
377 pub version: ProtocolV1,
378 pub issuer_id: IssuerId,
379 pub info: Value,
380 #[serde_as(as = "Base64UrlUnpadded")]
384 pub blind_signature: PbrsaBlindSignature,
385}
386
387#[cfg(test)]
388mod tests {
389 use blind_rsa_signatures::Signature as PbrsaSignature;
390 use serde_json::json;
391 use sha2::Digest;
392
393 use super::*;
394
395 fn credential() -> SignedCredential {
396 SignedCredential {
397 version: ProtocolV1,
398 credential: Credential {
399 issuer_id_pubkey: IssuerId(nostr::PublicKey::from_byte_array([1u8; 32])),
400 info: json!({
401 "z": 1,
402 "a": {
403 "b": true,
404 "a": false,
405 },
406 }),
407 blind_msg: json!({
408 "holder": "alice",
409 "nonce": 7,
410 }),
411 },
412 proof: CredentialProof {
413 signature: PbrsaSignature(vec![1, 2, 3, 4]),
414 },
415 }
416 }
417
418 #[test]
419 fn digest_hashes_domain_separator_and_canonical_credential_json() {
420 let credential = credential();
421
422 let canonical = canonicalize_credential(&credential.credential).unwrap();
423 let expected = Sha256::new()
424 .chain_update(CREDENTIAL_DIGEST_DOMAIN_SEPARATOR)
425 .chain_update(canonical)
426 .finalize();
427
428 assert_eq!(credential.credential.digest().unwrap(), expected);
429 }
430
431 #[test]
432 fn digest_excludes_signature() {
433 let mut first = credential();
434 let mut second = credential();
435 first.proof.signature = PbrsaSignature(vec![1, 2, 3, 4]);
436 second.proof.signature = PbrsaSignature(vec![1, 2, 3, 5]);
437
438 assert_eq!(
439 first.credential.digest().unwrap(),
440 second.credential.digest().unwrap()
441 );
442 }
443}