ghostkey_common/lib.rs
1use freenet_stdlib::prelude::*;
2use serde::{Deserialize, Serialize};
3
4#[cfg(feature = "crypto")]
5use ed25519_dalek::VerifyingKey;
6
7/// Compute fingerprint for a ghostkey verifying key.
8/// First 8 bytes of BLAKE3(verifying_key_bytes), base58-encoded.
9#[cfg(feature = "crypto")]
10pub fn fingerprint(verifying_key: &VerifyingKey) -> String {
11 let hash = blake3::hash(verifying_key.as_bytes());
12 bs58::encode(&hash.as_bytes()[..8]).into_string()
13}
14
15/// Serialize a value to CBOR bytes.
16pub fn to_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
17 let mut buf = Vec::new();
18 ciborium::into_writer(value, &mut buf).map_err(|e| format!("CBOR serialize: {e}"))?;
19 Ok(buf)
20}
21
22/// Deserialize a value from CBOR bytes.
23pub fn from_cbor<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, String> {
24 ciborium::from_reader(bytes).map_err(|e| format!("CBOR deserialize: {e}"))
25}
26
27/// Who requested a ghostkey operation. Runtime-attested, can't be spoofed.
28#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum SignatureRequestor {
31 /// A web application (UI) backed by this contract.
32 WebApp(ContractInstanceId),
33 /// Another delegate on the same node.
34 Delegate(DelegateKey),
35}
36
37/// What an authorised caller is allowed to do with a ghostkey.
38///
39/// A grant carries a set of scopes. The vault auto-grants itself every
40/// scope when it imports a key. Third-party apps can request access via
41/// `RequestAnyAccess`, which (on user approval) grants only
42/// `{ReadPublic, Sign}` -- enough to read the public certificate and sign
43/// messages, but not enough to extract the private key or destroy the
44/// identity. Apps that need higher privileges are deliberately routed
45/// through the vault, where the user is rendering the management UI.
46#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
47#[non_exhaustive]
48pub enum GhostkeyScope {
49 /// Read public certificate and metadata. Granted alongside `Sign`
50 /// because every signing UI also wants to display the public cert.
51 ReadPublic,
52 /// Sign messages with the private key. Implies `ReadPublic` in
53 /// practice (a verifier needs the cert), but gating is per-scope so
54 /// the grant intent is explicit.
55 Sign,
56 /// Export the private signing key. Catastrophic if granted to a
57 /// third-party app -- the recipient becomes able to sign as the
58 /// user offline. Only ever granted to the vault.
59 Export,
60 /// Delete the ghostkey or rewrite its label. Only ever granted to
61 /// the vault.
62 Delete,
63 /// Manage permissions for this ghostkey: grant/revoke other apps'
64 /// access. The vault gets this on import; third-party apps never
65 /// get it via `RequestAnyAccess`.
66 Admin,
67}
68
69/// What the ghostkey delegate actually signs. The raw payload is never signed
70/// alone -- always wrapped with the attested caller identity.
71#[derive(Serialize, Deserialize, Debug, Clone)]
72pub struct ScopedPayload {
73 pub requestor: SignatureRequestor,
74 pub payload: Vec<u8>,
75}
76
77/// Summary info about a stored ghostkey.
78#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
79pub struct GhostKeyInfo {
80 pub fingerprint: String,
81 pub label: Option<String>,
82 /// The notary certificate info field (encodes donation tier).
83 /// Historically called `delegate_info` — the wire-format key is frozen
84 /// via `#[serde(rename)]` for backward compat with stored state.
85 /// See freenet/web#24.
86 #[serde(rename = "delegate_info")]
87 pub notary_info: String,
88 /// Ed25519 verifying key bytes (32 bytes). Added in 0.2.2 for dapps
89 /// that need the raw key (e.g. Harvest store contract parameters).
90 #[serde(default)]
91 pub verifying_key_bytes: Option<Vec<u8>>,
92 /// Whether the user has ever exported this identity.
93 ///
94 /// On most nodes the vault holds the only copy of a ghostkey, so an
95 /// identity that has never left it is one lost disk away from gone. The
96 /// vault marks un-exported identities so the reminder sits where someone
97 /// is looking at something they own, rather than mid-purchase where it is
98 /// just an obstacle between them and finishing.
99 ///
100 /// `#[serde(default)]` so a record written by an older delegate reads back
101 /// as "not backed up" -- the safe direction, since over-warning costs a
102 /// nudge and under-warning costs the key.
103 #[serde(default)]
104 pub backed_up: bool,
105}
106
107/// A ghostkey exported for backup (includes private signing key).
108#[derive(Serialize, Deserialize, Debug, Clone)]
109pub struct ExportedGhostKey {
110 pub fingerprint: String,
111 pub certificate_pem: String,
112 pub signing_key_pem: String,
113 pub label: Option<String>,
114 #[serde(rename = "delegate_info")]
115 pub notary_info: String,
116}
117
118/// Requests from UI or other delegates to the ghostkey delegate.
119#[derive(Serialize, Deserialize, Debug, Clone)]
120#[non_exhaustive]
121pub enum GhostkeyRequest {
122 /// Import a ghostkey from PEM-armored certificate and signing key.
123 /// If master_verifying_key_pem is None, uses the hardcoded Freenet master key.
124 ImportGhostKey {
125 certificate_pem: String,
126 signing_key_pem: String,
127 #[serde(default)]
128 master_verifying_key_pem: Option<String>,
129 },
130 /// List all stored ghostkeys.
131 ListGhostKeys,
132 /// Get details for a specific ghostkey.
133 GetGhostKey { fingerprint: String },
134 /// Get just the public certificate (for sharing with counterparties).
135 GetCertificate { fingerprint: String },
136 /// Delete a stored ghostkey.
137 DeleteGhostKey { fingerprint: String },
138 /// Set a user-friendly label.
139 SetLabel { fingerprint: String, label: String },
140 /// Sign a message with a specific ghostkey. The delegate scopes the
141 /// signature to the requestor.
142 SignMessage {
143 fingerprint: String,
144 message: Vec<u8>,
145 },
146 /// Sign a message with the user's default ghostkey (highest-tier key,
147 /// or user-overridden via SetDefaultKey). Apps should prefer this over
148 /// SignMessage -- it avoids needing to know about specific fingerprints.
149 SignWithDefault { message: Vec<u8> },
150 /// Set which ghostkey is the default for signing.
151 SetDefaultKey { fingerprint: String },
152 /// Get the current default ghostkey fingerprint.
153 ///
154 /// Returns `DefaultKeyResult { fingerprint: None }` when the caller has no
155 /// `Sign` grant on any key, which is NOT the same as the user having no
156 /// ghostkey -- use `HasIdentity` for that question. This request never
157 /// prompts: it is a question, and an app must not be able to put a dialog
158 /// in front of the user just by asking one. `SignWithDefault` is the one
159 /// that prompts, because it acts.
160 GetDefaultKey,
161 /// Verify a signed message produced by this delegate.
162 VerifySignedMessage { signed_message: Vec<u8> },
163 /// Export a ghostkey's certificate and signing key for backup.
164 /// Security-sensitive: returns the private signing key.
165 ExportGhostKey { fingerprint: String },
166 /// Export all ghostkeys for backup.
167 ExportAllGhostKeys,
168 /// Grant an application or delegate permission to use a ghostkey.
169 GrantPermission {
170 fingerprint: String,
171 requestor: SignatureRequestor,
172 },
173 /// Revoke a previously granted permission.
174 RevokePermission {
175 fingerprint: String,
176 requestor: SignatureRequestor,
177 },
178 /// List permissions for a ghostkey.
179 ListPermissions { fingerprint: String },
180 /// Debug: force a permission prompt regardless of existing permissions.
181 TestPermissionPrompt { fingerprint: String },
182 /// A third-party app asks for any one of the user's ghostkeys. The
183 /// delegate emits a user prompt that lets the user pick a key (or
184 /// deny). On approval the delegate grants `{ReadPublic, Sign}` to
185 /// the requesting app for the chosen fingerprint and replies with a
186 /// single-element `GhostKeyList` containing that key.
187 ///
188 /// The request takes no fields on purpose: the only identifier the
189 /// user sees in the prompt is the runtime-attested requestor (a
190 /// truncated contract id). Letting the app supply free text would
191 /// open a phishing surface (a hostile app could write text designed
192 /// to look like Freenet UI chrome). Apps that want to communicate
193 /// purpose to the user should do so in their own UI before this
194 /// flow runs.
195 RequestAnyAccess,
196 /// Ask whether the user holds any ghostkey at all, WITHOUT prompting.
197 ///
198 /// Apps need this and today have no way to get it. `RequestAnyAccess`
199 /// always prompts, so it cannot be polled. `ListGhostKeys` is filtered by
200 /// permission, so an app with no grant yet sees an empty list and cannot
201 /// tell "the user has none" from "I have not been granted access".
202 ///
203 /// The motivating case is the purchase round trip: an app that sends a
204 /// user off to buy a ghostkey wants to notice when they come back, and
205 /// polling a prompt is not an option.
206 ///
207 /// What this discloses without consent is a count. That is more than the
208 /// bare existence bit `NoIdentityAvailable` already leaks to anyone who
209 /// asks for a signature, and the trade is deliberate: no fingerprints,
210 /// labels or tiers are exposed, a count cannot be correlated across users,
211 /// and the alternative is that the vault cannot tell a half-lost identity
212 /// from a healthy one.
213 HasIdentity,
214 /// Record that the user has exported this identity, so the vault can stop
215 /// warning that it is the only copy. Requires `Export` scope, so only the
216 /// vault can set it -- a third-party app must not be able to silence a
217 /// warning about a key it does not hold a backup of.
218 MarkBackedUp { fingerprint: String },
219}
220
221/// Responses from the ghostkey delegate.
222#[derive(Serialize, Deserialize, Debug, Clone)]
223#[non_exhaustive]
224pub enum GhostkeyResponse {
225 ImportResult {
226 fingerprint: String,
227 #[serde(rename = "delegate_info")]
228 notary_info: String,
229 },
230 GhostKeyList {
231 keys: Vec<GhostKeyInfo>,
232 },
233 GhostKeyDetail {
234 fingerprint: String,
235 certificate_pem: String,
236 label: Option<String>,
237 #[serde(rename = "delegate_info")]
238 notary_info: String,
239 },
240 Certificate {
241 fingerprint: String,
242 certificate_pem: String,
243 },
244 SignResult {
245 /// CBOR-serialized ScopedPayload
246 scoped_payload: Vec<u8>,
247 /// Ed25519 signature over the scoped_payload bytes
248 signature: Vec<u8>,
249 /// The certificate PEM, so the verifier has the full chain
250 certificate_pem: String,
251 },
252 DefaultKeyResult {
253 fingerprint: Option<String>,
254 },
255 DefaultKeySet {
256 fingerprint: String,
257 },
258 VerifyResult {
259 valid: bool,
260 signer_fingerprint: Option<String>,
261 #[serde(rename = "delegate_info")]
262 notary_info: Option<String>,
263 requestor: Option<SignatureRequestor>,
264 message: Option<Vec<u8>>,
265 },
266 Deleted {
267 fingerprint: String,
268 },
269 LabelSet {
270 fingerprint: String,
271 label: String,
272 },
273 PermissionGranted {
274 fingerprint: String,
275 requestor: SignatureRequestor,
276 },
277 PermissionRevoked {
278 fingerprint: String,
279 requestor: SignatureRequestor,
280 },
281 PermissionList {
282 fingerprint: String,
283 requestors: Vec<SignatureRequestor>,
284 },
285 ExportResult {
286 fingerprint: String,
287 certificate_pem: String,
288 signing_key_pem: String,
289 label: Option<String>,
290 },
291 ExportAllResult {
292 keys: Vec<ExportedGhostKey>,
293 },
294 PermissionDenied {
295 fingerprint: String,
296 requestor: SignatureRequestor,
297 },
298 /// Permission denied for a request that didn't name a specific
299 /// fingerprint -- today this means the user denied a
300 /// `RequestAnyAccess` prompt. Distinct from `PermissionDenied` so
301 /// callers don't have to invent a placeholder fingerprint to
302 /// pattern-match.
303 AccessDenied {
304 requestor: SignatureRequestor,
305 },
306 /// The user has no ghostkeys. Apps should direct the user to
307 /// freenet.org/ghostkey to purchase one.
308 ///
309 /// It is NOT returned merely because the caller lacks permission —
310 /// `SignWithDefault` prompts the user instead when the vault holds keys
311 /// the caller has no grant on. So an app can treat this as "offer to buy
312 /// one" without first checking whether it was really a permissions
313 /// problem.
314 ///
315 /// Precisely, it means no identity is *available to sign with*: either the
316 /// vault is empty, or every identity in it has lost its signing key. Use
317 /// `HasIdentity` to tell those apart — its `unusable` count is non-zero in
318 /// the second case, which is worth a different message, since buying
319 /// another key is not what that user needs.
320 NoIdentityAvailable,
321 /// Reply to `HasIdentity`. Counts only — no fingerprints, labels or tiers.
322 IdentityPresence {
323 /// Identities that can actually sign: certificate AND signing key both
324 /// present.
325 usable: usize,
326 /// Identities whose certificate loads but whose signing key is gone.
327 /// These still appear in `ListGhostKeys`, which never checks for the
328 /// signing key, so without this count a half-lost identity looks
329 /// perfectly healthy right up until it fails to sign.
330 unusable: usize,
331 },
332 /// Confirms `MarkBackedUp`.
333 BackedUpMarked {
334 fingerprint: String,
335 },
336 /// The requested ghostkey fingerprint was not found.
337 KeyNotFound {
338 fingerprint: String,
339 },
340 /// Generic error for unexpected failures.
341 Error {
342 message: String,
343 },
344}