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}
93
94/// A ghostkey exported for backup (includes private signing key).
95#[derive(Serialize, Deserialize, Debug, Clone)]
96pub struct ExportedGhostKey {
97    pub fingerprint: String,
98    pub certificate_pem: String,
99    pub signing_key_pem: String,
100    pub label: Option<String>,
101    #[serde(rename = "delegate_info")]
102    pub notary_info: String,
103}
104
105/// Requests from UI or other delegates to the ghostkey delegate.
106#[derive(Serialize, Deserialize, Debug, Clone)]
107#[non_exhaustive]
108pub enum GhostkeyRequest {
109    /// Import a ghostkey from PEM-armored certificate and signing key.
110    /// If master_verifying_key_pem is None, uses the hardcoded Freenet master key.
111    ImportGhostKey {
112        certificate_pem: String,
113        signing_key_pem: String,
114        #[serde(default)]
115        master_verifying_key_pem: Option<String>,
116    },
117    /// List all stored ghostkeys.
118    ListGhostKeys,
119    /// Get details for a specific ghostkey.
120    GetGhostKey { fingerprint: String },
121    /// Get just the public certificate (for sharing with counterparties).
122    GetCertificate { fingerprint: String },
123    /// Delete a stored ghostkey.
124    DeleteGhostKey { fingerprint: String },
125    /// Set a user-friendly label.
126    SetLabel { fingerprint: String, label: String },
127    /// Sign a message with a specific ghostkey. The delegate scopes the
128    /// signature to the requestor.
129    SignMessage {
130        fingerprint: String,
131        message: Vec<u8>,
132    },
133    /// Sign a message with the user's default ghostkey (highest-tier key,
134    /// or user-overridden via SetDefaultKey). Apps should prefer this over
135    /// SignMessage -- it avoids needing to know about specific fingerprints.
136    SignWithDefault { message: Vec<u8> },
137    /// Set which ghostkey is the default for signing.
138    SetDefaultKey { fingerprint: String },
139    /// Get the current default ghostkey fingerprint.
140    GetDefaultKey,
141    /// Verify a signed message produced by this delegate.
142    VerifySignedMessage { signed_message: Vec<u8> },
143    /// Export a ghostkey's certificate and signing key for backup.
144    /// Security-sensitive: returns the private signing key.
145    ExportGhostKey { fingerprint: String },
146    /// Export all ghostkeys for backup.
147    ExportAllGhostKeys,
148    /// Grant an application or delegate permission to use a ghostkey.
149    GrantPermission {
150        fingerprint: String,
151        requestor: SignatureRequestor,
152    },
153    /// Revoke a previously granted permission.
154    RevokePermission {
155        fingerprint: String,
156        requestor: SignatureRequestor,
157    },
158    /// List permissions for a ghostkey.
159    ListPermissions { fingerprint: String },
160    /// Debug: force a permission prompt regardless of existing permissions.
161    TestPermissionPrompt { fingerprint: String },
162    /// A third-party app asks for any one of the user's ghostkeys. The
163    /// delegate emits a user prompt that lets the user pick a key (or
164    /// deny). On approval the delegate grants `{ReadPublic, Sign}` to
165    /// the requesting app for the chosen fingerprint and replies with a
166    /// single-element `GhostKeyList` containing that key.
167    ///
168    /// The request takes no fields on purpose: the only identifier the
169    /// user sees in the prompt is the runtime-attested requestor (a
170    /// truncated contract id). Letting the app supply free text would
171    /// open a phishing surface (a hostile app could write text designed
172    /// to look like Freenet UI chrome). Apps that want to communicate
173    /// purpose to the user should do so in their own UI before this
174    /// flow runs.
175    RequestAnyAccess,
176}
177
178/// Responses from the ghostkey delegate.
179#[derive(Serialize, Deserialize, Debug, Clone)]
180#[non_exhaustive]
181pub enum GhostkeyResponse {
182    ImportResult {
183        fingerprint: String,
184        #[serde(rename = "delegate_info")]
185        notary_info: String,
186    },
187    GhostKeyList {
188        keys: Vec<GhostKeyInfo>,
189    },
190    GhostKeyDetail {
191        fingerprint: String,
192        certificate_pem: String,
193        label: Option<String>,
194        #[serde(rename = "delegate_info")]
195        notary_info: String,
196    },
197    Certificate {
198        fingerprint: String,
199        certificate_pem: String,
200    },
201    SignResult {
202        /// CBOR-serialized ScopedPayload
203        scoped_payload: Vec<u8>,
204        /// Ed25519 signature over the scoped_payload bytes
205        signature: Vec<u8>,
206        /// The certificate PEM, so the verifier has the full chain
207        certificate_pem: String,
208    },
209    DefaultKeyResult {
210        fingerprint: Option<String>,
211    },
212    DefaultKeySet {
213        fingerprint: String,
214    },
215    VerifyResult {
216        valid: bool,
217        signer_fingerprint: Option<String>,
218        #[serde(rename = "delegate_info")]
219        notary_info: Option<String>,
220        requestor: Option<SignatureRequestor>,
221        message: Option<Vec<u8>>,
222    },
223    Deleted {
224        fingerprint: String,
225    },
226    LabelSet {
227        fingerprint: String,
228        label: String,
229    },
230    PermissionGranted {
231        fingerprint: String,
232        requestor: SignatureRequestor,
233    },
234    PermissionRevoked {
235        fingerprint: String,
236        requestor: SignatureRequestor,
237    },
238    PermissionList {
239        fingerprint: String,
240        requestors: Vec<SignatureRequestor>,
241    },
242    ExportResult {
243        fingerprint: String,
244        certificate_pem: String,
245        signing_key_pem: String,
246        label: Option<String>,
247    },
248    ExportAllResult {
249        keys: Vec<ExportedGhostKey>,
250    },
251    PermissionDenied {
252        fingerprint: String,
253        requestor: SignatureRequestor,
254    },
255    /// Permission denied for a request that didn't name a specific
256    /// fingerprint -- today this means the user denied a
257    /// `RequestAnyAccess` prompt. Distinct from `PermissionDenied` so
258    /// callers don't have to invent a placeholder fingerprint to
259    /// pattern-match.
260    AccessDenied {
261        requestor: SignatureRequestor,
262    },
263    /// The user has no ghostkeys. Apps should direct the user to
264    /// freenet.org/ghostkey to purchase one.
265    NoIdentityAvailable,
266    /// The requested ghostkey fingerprint was not found.
267    KeyNotFound {
268        fingerprint: String,
269    },
270    /// Generic error for unexpected failures.
271    Error {
272        message: String,
273    },
274}