Skip to main content

peerbadge_protocol/
issuer.rs

1//! Issuer-side PBRSA issuance operations.
2
3#[cfg(not(feature = "sys-rng"))]
4use blind_rsa_signatures::DefaultRng;
5use blind_rsa_signatures::{
6    pbrsa::PartiallyBlindKeyPairSha384PSSDeterministic,
7    reexports::rand::{
8        self,
9        rand_core::{Infallible, TryCryptoRng, TryRng, UnwrapErr},
10        rngs::SysRng,
11    },
12};
13use serde_json::Value;
14
15use crate::{
16    canonicalize_pbrsa_info, CredentialsError, IssuanceRequest, IssuanceResponse, Issuer,
17    IssuerAuthority, IssuerId, IssuerSecretKeys, ProtocolV1, Revocation, RevocationLocation,
18    RevocationProof, SchnorrSignatureProof, SignedCredential, SignedRevocation,
19};
20
21pub const ISSUER_MODULUS_BITS: usize = 2048;
22const KEYGEN_PROGRESS_RANDOM_DRAWS: u64 = 100_000;
23
24fn default_pbrsa_rng() -> impl TryCryptoRng<Error = Infallible> {
25    #[cfg(feature = "sys-rng")]
26    {
27        UnwrapErr(SysRng)
28    }
29
30    #[cfg(not(feature = "sys-rng"))]
31    {
32        DefaultRng
33    }
34}
35
36struct KeygenProgressRng<R> {
37    inner: R,
38    rng_strategy: &'static str,
39    random_draws: u64,
40    next_progress_log_at: u64,
41}
42
43impl<R> KeygenProgressRng<R> {
44    fn new(inner: R, rng_strategy: &'static str) -> Self {
45        tracing::info!(rng_strategy, "issuer RSA keygen started");
46        Self {
47            inner,
48            rng_strategy,
49            random_draws: 0,
50            next_progress_log_at: KEYGEN_PROGRESS_RANDOM_DRAWS,
51        }
52    }
53
54    fn random_draws(&self) -> u64 {
55        self.random_draws
56    }
57
58    fn record_random_draw(&mut self) {
59        self.random_draws += 1;
60        if self.random_draws >= self.next_progress_log_at {
61            tracing::info!(
62                rng_strategy = self.rng_strategy,
63                random_draws = self.random_draws,
64                "issuer RSA keygen still running"
65            );
66            self.next_progress_log_at += KEYGEN_PROGRESS_RANDOM_DRAWS;
67        }
68    }
69}
70
71impl<R: TryRng> TryRng for KeygenProgressRng<R> {
72    type Error = R::Error;
73
74    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
75        let value = self.inner.try_next_u32()?;
76        self.record_random_draw();
77        Ok(value)
78    }
79
80    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
81        let value = self.inner.try_next_u64()?;
82        self.record_random_draw();
83        Ok(value)
84    }
85
86    fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
87        self.inner.try_fill_bytes(dst)?;
88        self.record_random_draw();
89        Ok(())
90    }
91}
92
93impl<R: TryCryptoRng> TryCryptoRng for KeygenProgressRng<R> {}
94
95/// Runtime issuer context containing issuer identity and PBRSA signing key.
96#[derive(Clone)]
97pub struct IssuerContext {
98    identity_keys: nostr::Keys,
99    key_pair: PartiallyBlindKeyPairSha384PSSDeterministic,
100}
101
102impl IssuerContext {
103    /// Generate an issuer context with fresh Nostr identity and PBRSA key pairs.
104    #[cfg(not(feature = "sys-rng"))]
105    pub fn generate() -> Result<Self, CredentialsError> {
106        Self::generate_with_thread_rng()
107    }
108
109    /// Generate an issuer context with fresh Nostr identity and PBRSA key pairs.
110    #[cfg(feature = "sys-rng")]
111    pub fn generate() -> Result<Self, CredentialsError> {
112        Self::generate_with_system_rng()
113    }
114
115    /// Generate an issuer context using a thread-local CSPRNG seeded from the system RNG.
116    pub fn generate_with_thread_rng() -> Result<Self, CredentialsError> {
117        Self::generate_with_rng_source("thread_rng", rand::rng())
118    }
119
120    /// Generate an issuer context using direct system randomness for each keygen draw.
121    pub fn generate_with_system_rng() -> Result<Self, CredentialsError> {
122        Self::generate_with_rng_source("system_rng", UnwrapErr(SysRng))
123    }
124
125    fn generate_with_rng_source<R>(
126        rng_strategy: &'static str,
127        rng: R,
128    ) -> Result<Self, CredentialsError>
129    where
130        R: TryCryptoRng<Error = Infallible>,
131    {
132        let span = tracing::info_span!(
133            "issuer_rsa_keygen",
134            modulus_bits = ISSUER_MODULUS_BITS,
135            rng_strategy
136        );
137        let _span_guard = span.enter();
138        let mut rng = KeygenProgressRng::new(rng, rng_strategy);
139        let generated = Self::generate_with_rng(nostr::Keys::generate(), &mut rng);
140        match &generated {
141            Ok(_) => tracing::info!(
142                rng_strategy,
143                random_draws = rng.random_draws(),
144                "issuer RSA keygen finished"
145            ),
146            Err(error) => tracing::warn!(
147                rng_strategy,
148                random_draws = rng.random_draws(),
149                %error,
150                "issuer RSA keygen failed"
151            ),
152        }
153        generated
154    }
155
156    pub(crate) fn generate_with_rng(
157        identity_keys: nostr::Keys,
158        rng: &mut (impl blind_rsa_signatures::reexports::rsa::rand_core::CryptoRng + ?Sized),
159    ) -> Result<Self, CredentialsError> {
160        Ok(Self {
161            identity_keys,
162            key_pair: PartiallyBlindKeyPairSha384PSSDeterministic::generate(
163                rng,
164                ISSUER_MODULUS_BITS,
165            )?,
166        })
167    }
168
169    /// Build and sign this issuer's public metadata.
170    ///
171    /// The returned authority binds the issuer's derived Nostr identity key to the
172    /// current PBRSA issuance public key and the supplied revocation locations.
173    pub fn issuer_authority(
174        &self,
175        revocation: Vec<RevocationLocation>,
176    ) -> Result<IssuerAuthority, CredentialsError> {
177        self.issuer_authority_with_rng(revocation, &mut nostr::secp256k1::rand::rngs::OsRng)
178    }
179
180    pub(crate) fn issuer_authority_with_rng(
181        &self,
182        revocation: Vec<RevocationLocation>,
183        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
184    ) -> Result<IssuerAuthority, CredentialsError> {
185        let issuer = Issuer {
186            issuer_id_pubkey: self.issuer_id(),
187            issuance_key: self.key_pair.pk.clone(),
188            revocation,
189        };
190        let signature = self.sign_identity_digest_with_rng(issuer.digest()?, rng);
191
192        Ok(IssuerAuthority {
193            version: ProtocolV1,
194            issuer,
195            proof: SchnorrSignatureProof { signature },
196        })
197    }
198
199    fn issuer_id(&self) -> IssuerId {
200        IssuerId(self.identity_keys.public_key())
201    }
202
203    /// Export this issuer's identity and issuance secret keys.
204    pub fn export_secret_key(&self) -> Result<IssuerSecretKeys, CredentialsError> {
205        Ok(IssuerSecretKeys {
206            issuer_id_secret_key: self.identity_keys.secret_key().to_secret_hex(),
207            issuance_secret_key: self.key_pair.sk.to_der()?,
208        })
209    }
210
211    /// Import an issuer context from previously exported secret keys.
212    pub fn import_secret_key(secret_key: &IssuerSecretKeys) -> Result<Self, CredentialsError> {
213        let identity_keys = nostr::Keys::parse(&secret_key.issuer_id_secret_key)?;
214        let secret_key =
215            blind_rsa_signatures::pbrsa::PartiallyBlindSecretKeySha384PSSDeterministic::from_der(
216                &secret_key.issuance_secret_key,
217            )?;
218        let public_key = secret_key.public_key()?;
219        Ok(Self {
220            identity_keys,
221            key_pair: PartiallyBlindKeyPairSha384PSSDeterministic {
222                pk: public_key,
223                sk: secret_key,
224            },
225        })
226    }
227
228    /// Issue a blind signature over a holder issuance request.
229    pub fn issue_credential(
230        &self,
231        info: Value,
232        request: &IssuanceRequest,
233    ) -> Result<IssuanceResponse, CredentialsError> {
234        self.issue_credential_with_rng(info, request, &mut default_pbrsa_rng())
235    }
236
237    pub(crate) fn issue_credential_with_rng(
238        &self,
239        info: Value,
240        request: &IssuanceRequest,
241        rng: &mut (impl blind_rsa_signatures::reexports::rsa::rand_core::TryCryptoRng + ?Sized),
242    ) -> Result<IssuanceResponse, CredentialsError> {
243        let issuer_id = self.issuer_id();
244        let metadata = canonicalize_pbrsa_info(ProtocolV1, &issuer_id, &info)?;
245        let secret_key = self.key_pair.derive_secret_key_for_metadata(&metadata)?;
246        Ok(IssuanceResponse {
247            version: ProtocolV1,
248            issuer_id,
249            info,
250            blind_signature: secret_key.blind_sign_with_rng(rng, &request.blinded_message)?,
251        })
252    }
253
254    /// Build and sign the revocation for a finalized credential issued by this issuer.
255    ///
256    /// This computes the finalized credential digest and binds it to this issuer
257    /// identity. It does not publish or transport the revocation; those concerns
258    /// live outside the core protocol.
259    pub fn revoke_credential(
260        &self,
261        credential: &SignedCredential,
262    ) -> Result<SignedRevocation, CredentialsError> {
263        self.revoke_credential_with_rng(credential, &mut nostr::secp256k1::rand::rngs::OsRng)
264    }
265
266    pub(crate) fn revoke_credential_with_rng(
267        &self,
268        credential: &SignedCredential,
269        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
270    ) -> Result<SignedRevocation, CredentialsError> {
271        let issuer_id = self.issuer_id();
272        if credential.credential.issuer_id_pubkey != issuer_id {
273            return Err(CredentialsError::IssuerIdMismatch);
274        }
275
276        let revocation = Revocation {
277            credential_digest: crate::CredentialDigest(credential.credential.digest()?),
278        };
279
280        let signature = self.sign_identity_digest_with_rng(revocation.digest()?, rng);
281
282        Ok(SignedRevocation {
283            version: ProtocolV1,
284            revocation,
285            proof: RevocationProof {
286                issuer_id_pubkey: issuer_id,
287                signature,
288            },
289        })
290    }
291
292    fn sign_identity_digest_with_rng(
293        &self,
294        digest: sha2::digest::Output<sha2::Sha256>,
295        rng: &mut (impl nostr::secp256k1::rand::Rng + nostr::secp256k1::rand::CryptoRng),
296    ) -> nostr::secp256k1::schnorr::Signature {
297        self.identity_keys.sign_schnorr_with_ctx(
298            nostr::SECP256K1,
299            &nostr::secp256k1::Message::from_digest(digest.into()),
300            rng,
301        )
302    }
303}