Skip to main content

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}