1use base64::{engine::general_purpose::STANDARD, Engine};
4use serde::{Deserialize, Serialize};
5
6use crate::{pubky_routing, validation::invalid_data, PaykitError, PublicKey, Result};
7
8const KIND: &str = "paykit.noise_key_authorization";
9const VERSION: u8 = 1;
10const MAX_BYTES: usize = 4096;
11const SIGNING_DOMAIN: &[u8] = b"paykit.noise_key_authorization/v1\0";
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(try_from = "AuthorizationWire", into = "AuthorizationWire")]
20pub struct PaykitNoiseKeyAuthorization {
21 owner: PublicKey,
22 noise_public_key: PublicKey,
23 noise_static_public_key: [u8; 32],
24 key_generation: u64,
25 signature: String,
26}
27
28impl PaykitNoiseKeyAuthorization {
29 pub fn sign(
32 identity: &pubky::Keypair,
33 noise_secret_key: &[u8; 32],
34 key_generation: u64,
35 ) -> Result<Self> {
36 if key_generation == 0 {
37 return Err(PaykitError::Validation(
38 "Paykit key generation must be positive".into(),
39 ));
40 }
41 let owner = identity.public_key();
42 let noise_public_key = pubky::Keypair::from_secret(noise_secret_key).public_key();
43 let noise_static_public_key = pubky_noise::derive_static_public_key(noise_secret_key);
44 let signature = identity.sign(&signing_bytes(
45 &owner,
46 &noise_public_key,
47 &noise_static_public_key,
48 key_generation,
49 ));
50 Ok(Self {
51 owner,
52 noise_public_key,
53 noise_static_public_key,
54 key_generation,
55 signature: STANDARD.encode(signature.to_bytes()),
56 })
57 }
58
59 pub fn owner(&self) -> &PublicKey {
61 &self.owner
62 }
63
64 pub fn noise_public_key(&self) -> &PublicKey {
66 &self.noise_public_key
67 }
68
69 pub fn noise_static_public_key(&self) -> &[u8; 32] {
71 &self.noise_static_public_key
72 }
73
74 pub fn key_generation(&self) -> u64 {
76 self.key_generation
77 }
78
79 pub fn validate_against(&self, previous: &Self) -> Result<()> {
82 if self.owner != previous.owner
83 || self.key_generation < previous.key_generation
84 || (self.key_generation == previous.key_generation
85 && (self.noise_public_key != previous.noise_public_key
86 || self.noise_static_public_key != previous.noise_static_public_key))
87 {
88 return Err(invalid_data(
89 "Paykit Noise key authorization conflicts with the previously verified key",
90 None,
91 ));
92 }
93 Ok(())
94 }
95}
96
97fn signing_bytes(
99 owner: &PublicKey,
100 noise: &PublicKey,
101 noise_static: &[u8; 32],
102 generation: u64,
103) -> Vec<u8> {
104 [
105 SIGNING_DOMAIN,
106 owner.as_bytes(),
107 noise.as_bytes(),
108 noise_static,
109 &generation.to_be_bytes(),
110 ]
111 .concat()
112}
113
114#[derive(Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116struct AuthorizationWire {
117 version: u8,
118 kind: String,
119 owner: PublicKey,
120 noise_public_key: PublicKey,
121 noise_static_public_key: String,
122 key_generation: u64,
123 signature: String,
124}
125
126impl From<PaykitNoiseKeyAuthorization> for AuthorizationWire {
127 fn from(record: PaykitNoiseKeyAuthorization) -> Self {
128 Self {
129 version: VERSION,
130 kind: KIND.into(),
131 owner: record.owner,
132 noise_public_key: record.noise_public_key,
133 noise_static_public_key: hex::encode(record.noise_static_public_key),
134 key_generation: record.key_generation,
135 signature: record.signature,
136 }
137 }
138}
139
140impl TryFrom<AuthorizationWire> for PaykitNoiseKeyAuthorization {
141 type Error = PaykitError;
142
143 fn try_from(wire: AuthorizationWire) -> Result<Self> {
144 if wire.version != VERSION || wire.kind != KIND || wire.key_generation == 0 {
145 return Err(invalid_data(
146 "invalid Paykit Noise key authorization header",
147 None,
148 ));
149 }
150 let mut noise_static_public_key = [0; 32];
151 hex::decode_to_slice(&wire.noise_static_public_key, &mut noise_static_public_key)
152 .map_err(|err| invalid_data("invalid Noise static public key", Some(err.into())))?;
153 let bytes = STANDARD.decode(&wire.signature).map_err(|err| {
154 invalid_data(
155 "invalid Paykit Noise key authorization signature",
156 Some(err.into()),
157 )
158 })?;
159 let signature = bytes.as_slice().try_into().map_err(|_| {
160 invalid_data(
161 "invalid Paykit Noise key authorization signature length",
162 None,
163 )
164 })?;
165 wire.owner
166 .verify(
167 &signing_bytes(
168 &wire.owner,
169 &wire.noise_public_key,
170 &noise_static_public_key,
171 wire.key_generation,
172 ),
173 &signature,
174 )
175 .map_err(|err| {
176 invalid_data(
177 "Paykit Noise key authorization signature verification failed",
178 Some(err.into()),
179 )
180 })?;
181 Ok(Self {
182 owner: wire.owner,
183 noise_public_key: wire.noise_public_key,
184 noise_static_public_key,
185 key_generation: wire.key_generation,
186 signature: wire.signature,
187 })
188 }
189}
190
191fn parse(bytes: &[u8], owner: &PublicKey) -> Result<PaykitNoiseKeyAuthorization> {
192 let record: PaykitNoiseKeyAuthorization = serde_json::from_slice(bytes)
193 .map_err(|err| invalid_data("invalid Paykit Noise key authorization", Some(err.into())))?;
194 if record.owner() != owner {
195 return Err(invalid_data(
196 "Paykit Noise key authorization belongs to another identity",
197 None,
198 ));
199 }
200 Ok(record)
201}
202
203pub async fn get_paykit_noise_key_authorization(
209 storage: &pubky::PublicStorage,
210 owner: &PublicKey,
211) -> Result<Option<PaykitNoiseKeyAuthorization>> {
212 pubky_routing::fetch_text(
213 storage,
214 format!(
215 "{owner}{}",
216 pubky_routing::PAYKIT_NOISE_KEY_AUTHORIZATION_PATH
217 ),
218 "fetch Paykit Noise key authorization",
219 Some(MAX_BYTES),
220 )
221 .await?
222 .map(|body| parse(body.as_bytes(), owner))
223 .transpose()
224}
225
226pub async fn publish_paykit_noise_key_authorization(
235 session: &pubky::PubkySession,
236 record: &PaykitNoiseKeyAuthorization,
237) -> Result<()> {
238 if session.info().public_key() != record.owner() {
239 return Err(PaykitError::Validation(
240 "Noise key authorizer session belongs to another identity".into(),
241 ));
242 }
243 let path = pubky_routing::PAYKIT_NOISE_KEY_AUTHORIZATION_PATH;
244 pubky_routing::with_write_lock(session, path, |lock| async move {
245 let previous = match session.storage().get(path).await {
246 Ok(response) => {
247 let bytes = pubky_routing::read_bounded_body(
248 response,
249 MAX_BYTES,
250 "read current Noise key authorization",
251 )
252 .await?;
253 Some(parse(&bytes, record.owner())?)
254 }
255 Err(err) if pubky_routing::is_not_found(&err) => None,
256 Err(err) => {
257 return Err(PaykitError::Transport {
258 context: "read current Noise key authorization".into(),
259 source: err.into(),
260 })
261 }
262 };
263 if let Some(previous) = previous {
264 record.validate_against(&previous)?;
265 if record.key_generation == previous.key_generation {
266 return Ok(());
267 }
268 if previous.key_generation.checked_add(1) != Some(record.key_generation) {
269 return Err(PaykitError::Validation(
270 "Noise key authorization must advance one generation".into(),
271 ));
272 }
273 }
274 let body =
275 serde_json::to_vec(record).map_err(|err| PaykitError::Validation(err.to_string()))?;
276 session
277 .storage()
278 .put_locked(&lock, body)
279 .await
280 .map_err(|err| PaykitError::Transport {
281 context: "publish Paykit Noise key authorization".into(),
282 source: err.into(),
283 })?;
284 Ok(())
285 })
286 .await
287}
288
289#[cfg(test)]
290mod tests;