Skip to main content

peerbadge_protocol/
authorization.rs

1//! Holder authorization protocol types.
2//!
3//! These types describe holder-signed authorizations that allow an auxiliary
4//! subject key to present holder credentials without sharing the holder key.
5
6use serde::{Deserialize, Serialize};
7use sha2::{digest::Output, Digest, Sha256};
8use std::str::FromStr;
9
10use crate::{
11    canonical::canonicalize_holder_authorization,
12    types::{
13        verify_identity_signature_with_key, CredentialDigest, HolderId, ProtocolV1,
14        SchnorrSignatureProof, SignedCredential, Timestamp,
15    },
16    CredentialsError,
17};
18
19/// Domain separator for holder authorization identity signatures.
20pub const HOLDER_AUTHORIZATION_SIGNATURE_DOMAIN_SEPARATOR: &[u8] =
21    b"fedi-credential/holder-authorization-signature/v1\0";
22
23/// Public identity of the auxiliary key or actor authorized by the holder.
24///
25/// V1 keeps this as a Nostr public key, matching `IssuerId` and `HolderId`.
26#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct SubjectPubkey(pub nostr::PublicKey);
29
30impl FromStr for SubjectPubkey {
31    type Err = nostr::key::Error;
32
33    fn from_str(value: &str) -> Result<Self, Self::Err> {
34        nostr::PublicKey::parse(value).map(Self)
35    }
36}
37
38/// Request for a holder authorization.
39///
40/// An auxiliary subject key asks to act under the holder's identity. The holder
41/// chooses which credential to authorize separately when signing.
42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
43pub struct HolderAuthorizationRequest {
44    /// The auxiliary key or actor that the holder authorizes.
45    pub subject_pubkey: SubjectPubkey,
46}
47
48impl HolderAuthorizationRequest {
49    /// Convert this application input into the canonical statement to sign.
50    pub fn into_statement(
51        self,
52        holder_id_pubkey: HolderId,
53        issued_at: Timestamp,
54        credential: &SignedCredential,
55    ) -> Result<HolderAuthorizationStatement, CredentialsError> {
56        let credential_digest = CredentialDigest(credential.credential.digest()?);
57
58        Ok(HolderAuthorizationStatement {
59            holder_id_pubkey,
60            subject_pubkey: self.subject_pubkey,
61            credential_digest,
62            issued_at,
63        })
64    }
65}
66
67/// Unsigned statement authorizing an auxiliary public key to present credentials.
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69pub struct HolderAuthorizationStatement {
70    /// The holder making the authorization.
71    pub holder_id_pubkey: HolderId,
72
73    /// The auxiliary key or actor that the holder authorizes.
74    pub subject_pubkey: SubjectPubkey,
75
76    /// Credential digest this authorization grants the subject permission to present.
77    pub credential_digest: CredentialDigest,
78
79    /// Unix timestamp in seconds.
80    pub issued_at: Timestamp,
81}
82
83impl HolderAuthorizationStatement {
84    /// Compute the signature digest for this holder authorization statement.
85    pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
86        let canonical = canonicalize_holder_authorization(self)?;
87        Ok(Sha256::new()
88            .chain_update(HOLDER_AUTHORIZATION_SIGNATURE_DOMAIN_SEPARATOR)
89            .chain_update(canonical)
90            .finalize())
91    }
92}
93
94/// Holder-signed authorization.
95///
96/// This intentionally stays close to upstream `SignedCredential { version,
97/// credential, proof }`: a versioned signed claim plus a proof. The difference is
98/// that this is a direct holder identity signature over an unblinded statement,
99/// not an issuer PBRSA proof over a blind-issued credential.
100#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
101pub struct HolderAuthorization {
102    /// Protocol version for this shape.
103    pub version: ProtocolV1,
104
105    /// Statement signed by the holder.
106    pub authorization: HolderAuthorizationStatement,
107
108    /// Holder signature over canonical `authorization` with a versioned domain
109    /// separator such as `fedi-credential/holder-authorization-signature/v1\0`.
110    pub proof: SchnorrSignatureProof,
111}
112
113impl HolderAuthorization {
114    /// Compute the signature digest for this holder authorization payload.
115    pub fn digest(&self) -> Result<Output<Sha256>, CredentialsError> {
116        self.authorization.digest()
117    }
118
119    /// Verify this authorization's holder signature and return the statement.
120    pub fn verify(&self) -> Result<HolderAuthorizationStatement, CredentialsError> {
121        verify_identity_signature_with_key(
122            &self.authorization.holder_id_pubkey.0,
123            &self.proof.signature,
124            nostr::secp256k1::Message::from_digest(self.digest()?.into()),
125        )?;
126
127        Ok(self.authorization.clone())
128    }
129}