Skip to main content

peerbadge_protocol/
holder.rs

1//! Holder-side PBRSA issuance operations.
2
3use core::convert::Infallible;
4
5#[cfg(feature = "sys-rng")]
6use blind_rsa_signatures::reexports::rand::{rand_core::UnwrapErr, rngs::SysRng};
7#[cfg(not(feature = "sys-rng"))]
8use blind_rsa_signatures::DefaultRng;
9use blind_rsa_signatures::{
10    reexports::rand::rand_core::TryCryptoRng, BlindMessage, BlindingResult, MessageRandomizer,
11    Secret,
12};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use serde_with::serde_as;
16
17use crate::serde::Base64UrlUnpadded;
18use crate::{
19    canonicalize_pbrsa_blind_msg, canonicalize_pbrsa_info,
20    verifier::{credential_holder_id, verify_credential_with_key},
21    Credential, CredentialProof, CredentialsError, HolderAuthorization, HolderAuthorizationRequest,
22    HolderId, IssuanceRequest, IssuanceResponse, IssuerId, PbrsaPublicKey, ProtocolV1,
23    SchnorrSignatureProof, SignedCredential, Timestamp,
24};
25
26fn default_pbrsa_rng() -> impl TryCryptoRng<Error = Infallible> {
27    #[cfg(feature = "sys-rng")]
28    {
29        UnwrapErr(SysRng)
30    }
31
32    #[cfg(not(feature = "sys-rng"))]
33    {
34        DefaultRng
35    }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40enum PendingIssuanceStep {
41    WaitingForIssuerResponse,
42}
43
44/// Runtime holder context containing holder identity keys.
45#[derive(Clone)]
46pub struct HolderContext {
47    identity_keys: nostr::Keys,
48}
49
50impl HolderContext {
51    /// Generate a holder context with fresh Nostr identity keys.
52    pub fn generate() -> Self {
53        Self::generate_with_rng(&mut nostr::secp256k1::rand::rngs::OsRng)
54    }
55
56    pub(crate) fn generate_with_rng(
57        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
58    ) -> Self {
59        Self {
60            identity_keys: nostr::Keys::generate_with_rng(rng),
61        }
62    }
63
64    /// Import a holder context from a Nostr secret key string.
65    pub fn import_secret_key(secret_key: &str) -> Result<Self, CredentialsError> {
66        Ok(Self {
67            identity_keys: nostr::Keys::parse(secret_key)?,
68        })
69    }
70
71    /// Export this holder's Nostr secret key as a hex string.
72    pub fn export_secret_key(&self) -> String {
73        self.identity_keys.secret_key().to_secret_hex()
74    }
75
76    /// Return this holder's Nostr public key.
77    pub fn public_key(&self) -> nostr::PublicKey {
78        self.identity_keys.public_key()
79    }
80
81    /// Return this holder's protocol holder id.
82    pub fn holder_id(&self) -> HolderId {
83        HolderId(self.identity_keys.public_key())
84    }
85
86    /// Create a signed authorization allowing an auxiliary subject key to use a credential.
87    ///
88    /// The SDK derives this holder's id and credential digest from the supplied
89    /// credential before signing the canonical authorization statement. Consent
90    /// UI, storage, transport, and subject-key custody remain application
91    /// concerns.
92    pub fn authorize_credential_use(
93        &self,
94        request: HolderAuthorizationRequest,
95        credential: &SignedCredential,
96    ) -> Result<HolderAuthorization, CredentialsError> {
97        self.authorize_credential_use_at_time(request, credential, current_unix_timestamp()?)
98    }
99
100    /// Create a signed authorization using an explicit issuance timestamp.
101    pub fn authorize_credential_use_at_time(
102        &self,
103        request: HolderAuthorizationRequest,
104        credential: &SignedCredential,
105        issued_at: impl Into<Timestamp>,
106    ) -> Result<HolderAuthorization, CredentialsError> {
107        self.authorize_credential_use_with_rng_at_time(
108            request,
109            credential,
110            issued_at.into(),
111            &mut nostr::secp256k1::rand::rngs::OsRng,
112        )
113    }
114
115    pub(crate) fn authorize_credential_use_with_rng_at_time(
116        &self,
117        request: HolderAuthorizationRequest,
118        credential: &SignedCredential,
119        issued_at: impl Into<Timestamp>,
120        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
121    ) -> Result<HolderAuthorization, CredentialsError> {
122        let holder_id = self.holder_id();
123        if credential_holder_id(credential)? != holder_id {
124            return Err(CredentialsError::HolderIdMismatch);
125        }
126
127        let authorization = request.into_statement(holder_id, issued_at.into(), credential)?;
128        let signature = self.sign_identity_digest_with_rng(authorization.digest()?, rng);
129
130        Ok(HolderAuthorization {
131            version: ProtocolV1,
132            authorization,
133            proof: SchnorrSignatureProof { signature },
134        })
135    }
136
137    fn sign_identity_digest_with_rng(
138        &self,
139        digest: sha2::digest::Output<sha2::Sha256>,
140        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
141    ) -> nostr::secp256k1::schnorr::Signature {
142        self.identity_keys.sign_schnorr_with_ctx(
143            nostr::SECP256K1,
144            &nostr::secp256k1::Message::from_digest(digest.into()),
145            rng,
146        )
147    }
148}
149
150/// Holder-side pending issuance state.
151#[serde_as]
152#[derive(Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct PendingIssuance {
155    pub version: ProtocolV1,
156    step: PendingIssuanceStep,
157    pub issuer_id: IssuerId,
158    pub info: Value,
159    pub blind_msg: Value,
160    #[serde_as(as = "Base64UrlUnpadded")]
161    blind_message: Vec<u8>,
162    #[serde_as(as = "Base64UrlUnpadded")]
163    secret: Vec<u8>,
164    #[serde_as(as = "Option<Base64UrlUnpadded>")]
165    msg_randomizer: Option<Vec<u8>>,
166}
167
168impl PendingIssuance {
169    /// Create a holder issuance request and local pending state.
170    pub fn create_request(
171        issuer_public_key: &PbrsaPublicKey,
172        issuer_id: IssuerId,
173        info: Value,
174        blind_msg: Value,
175    ) -> Result<(IssuanceRequest, Self), CredentialsError> {
176        let mut rng = default_pbrsa_rng();
177
178        Self::create_request_with_rng(issuer_public_key, issuer_id, info, blind_msg, &mut rng)
179    }
180
181    pub(crate) fn create_request_with_rng(
182        issuer_public_key: &PbrsaPublicKey,
183        issuer_id: IssuerId,
184        info: Value,
185        blind_msg: Value,
186        rng: &mut (impl blind_rsa_signatures::reexports::rsa::rand_core::CryptoRng + ?Sized),
187    ) -> Result<(IssuanceRequest, Self), CredentialsError> {
188        let metadata = canonicalize_pbrsa_info(ProtocolV1, &issuer_id, &info)?;
189        let message = canonicalize_pbrsa_blind_msg(ProtocolV1, &blind_msg)?;
190        let public_key = issuer_public_key.derive_public_key_for_metadata(&metadata)?;
191        let blinding_result = public_key.blind(rng, &message, Some(&metadata))?;
192        let blinded_message = blinding_result.blind_message.clone();
193
194        let request = IssuanceRequest {
195            version: ProtocolV1,
196            blinded_message,
197        };
198        let pending = Self {
199            version: ProtocolV1,
200            step: PendingIssuanceStep::WaitingForIssuerResponse,
201            issuer_id,
202            info,
203            blind_msg,
204            blind_message: blinding_result.blind_message.0,
205            secret: blinding_result.secret.0,
206            msg_randomizer: blinding_result
207                .msg_randomizer
208                .map(|msg_randomizer| msg_randomizer.0.to_vec()),
209        };
210
211        Ok((request, pending))
212    }
213
214    /// Export app-storable holder-side pending issuance state.
215    ///
216    /// The exported state is sensitive issuance material. It is needed to
217    /// finalize one issuer response after a process or browser reload.
218    pub fn export_state(&self) -> Result<String, CredentialsError> {
219        Ok(serde_json::to_string(self)?)
220    }
221
222    /// Import app-stored holder-side pending issuance state.
223    pub fn import_state(state: &str) -> Result<Self, CredentialsError> {
224        serde_json::from_str(state).map_err(invalid_pending_issuance_state)
225    }
226
227    /// Finalize an issuer response into a holder credential.
228    pub fn finalize(
229        self,
230        issuer_public_key: &PbrsaPublicKey,
231        response: &IssuanceResponse,
232    ) -> Result<SignedCredential, CredentialsError> {
233        if response.issuer_id != self.issuer_id {
234            return Err(CredentialsError::IssuerIdMismatch);
235        }
236        if response.info != self.info {
237            return Err(CredentialsError::InfoMismatch);
238        }
239
240        let metadata = canonicalize_pbrsa_info(ProtocolV1, &self.issuer_id, &self.info)?;
241        let message = canonicalize_pbrsa_blind_msg(ProtocolV1, &self.blind_msg)?;
242        let public_key = issuer_public_key.derive_public_key_for_metadata(&metadata)?;
243        let blinding_result = self.blinding_result()?;
244        let signature = public_key.finalize(
245            &response.blind_signature,
246            &blinding_result,
247            &message,
248            Some(&metadata),
249        )?;
250        let credential = SignedCredential {
251            version: ProtocolV1,
252            credential: Credential {
253                issuer_id_pubkey: self.issuer_id,
254                info: self.info,
255                blind_msg: self.blind_msg,
256            },
257            proof: CredentialProof { signature },
258        };
259        verify_credential_with_key(issuer_public_key, &credential)?;
260        Ok(credential)
261    }
262
263    fn blinding_result(&self) -> Result<BlindingResult, CredentialsError> {
264        let msg_randomizer = self
265            .msg_randomizer
266            .as_ref()
267            .map(|msg_randomizer| {
268                let msg_randomizer: [u8; 32] =
269                    msg_randomizer.as_slice().try_into().map_err(|_| {
270                        CredentialsError::InvalidPendingIssuanceState(format!(
271                            "message randomizer must be 32 bytes, got {}",
272                            msg_randomizer.len()
273                        ))
274                    })?;
275                Ok::<MessageRandomizer, CredentialsError>(MessageRandomizer(msg_randomizer))
276            })
277            .transpose()?;
278
279        Ok(BlindingResult {
280            blind_message: BlindMessage(self.blind_message.clone()),
281            secret: Secret(self.secret.clone()),
282            msg_randomizer,
283        })
284    }
285}
286
287fn invalid_pending_issuance_state(error: impl ToString) -> CredentialsError {
288    CredentialsError::InvalidPendingIssuanceState(error.to_string())
289}
290
291pub(crate) fn current_unix_timestamp() -> Result<Timestamp, CredentialsError> {
292    std::time::SystemTime::now()
293        .duration_since(std::time::UNIX_EPOCH)
294        .map(|duration| Timestamp(duration.as_secs()))
295        .map_err(|_| CredentialsError::VerificationFailed)
296}