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}