{"id":49803325,"url":"https://github.com/freenet/ghostkeys","last_synced_at":"2026-05-12T16:10:46.974Z","repository":{"id":350877033,"uuid":"1206538169","full_name":"freenet/ghostkeys","owner":"freenet","description":"Ghostkey delegate and management UI for Freenet","archived":false,"fork":false,"pushed_at":"2026-05-05T14:34:43.000Z","size":191,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T16:14:14.668Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/freenet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-10T02:33:15.000Z","updated_at":"2026-05-05T14:35:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/freenet/ghostkeys","commit_stats":null,"previous_names":["freenet/ghostkeys"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/freenet/ghostkeys","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freenet%2Fghostkeys","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freenet%2Fghostkeys/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freenet%2Fghostkeys/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freenet%2Fghostkeys/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/freenet","download_url":"https://codeload.github.com/freenet/ghostkeys/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freenet%2Fghostkeys/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32946624,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-12T09:19:52.626Z","status":"ssl_error","status_checked_at":"2026-05-12T09:17:33.438Z","response_time":102,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-12T16:10:46.013Z","updated_at":"2026-05-12T16:10:46.966Z","avatar_url":"https://github.com/freenet.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ghostkeys\n\nA Freenet [delegate](https://freenet.org/resources/manual/components/delegates/) for managing [ghost key](https://freenet.org/ghostkey/) identities -- cryptographic certificates that prove trust without revealing identity.\n\nGhost keys solve a fundamental internet problem: how do you verify someone is trustworthy without knowing who they are? Through a blind signing protocol, users receive a cryptographic certificate signed by Freenet's server that can never be linked back to them. This enables spam prevention, bot blocking, and trust verification while preserving complete anonymity.\n\nThis delegate is a platform service. Any application running on Freenet -- a chat app, a forum, a marketplace -- can request signatures from the ghostkeys delegate on behalf of the user, with the user's explicit permission. The application never touches the private key; it just gets back a verifiable signature proving the user holds a legitimate ghost key.\n\nFor background, see [the introductory article](https://freenet.org/news/introducing-ghost-keys/) or [watch the interview with Ian Clarke](https://freenet.org/news/ghost-keys-ian-interview/).\n\n## Why a delegate?\n\nOn Freenet, [delegates](https://freenet.org/resources/manual/components/delegates/) are software agents that run on the user's machine and hold secrets on their behalf. Unlike traditional software encapsulation (which is just convention), delegates enforce real isolation at the platform level: the private state inside a delegate is not directly accessible to anything else. The only way to interact with a delegate is through its message interface, and the delegate can apply policy to every request.\n\nFreenet's runtime attests the identity of every caller -- when the ghostkeys delegate receives a request, it knows exactly which application or delegate sent it, and this identity can't be spoofed. This is what makes the permission and scoping systems described below possible.\n\nThis matters because ghost keys contain private signing keys. If those keys were held by the application itself, any app could sign anything, impersonate users, or leak keys. By placing them in a delegate, the keys stay in one secure place and applications request signatures through a controlled interface with user consent.\n\n## How ghost keys are created\n\n1. User makes a donation to Freenet\n2. Browser generates an [Ed25519](https://en.wikipedia.org/wiki/EdDSA) key pair\n3. The public key is [blinded](https://www.rfc-editor.org/rfc/rfc9474.html) and sent to the server\n4. Server verifies the donation, signs the blinded key with its RSA key, and returns it\n5. Browser unblinds the signature and combines it into a certificate\n6. The certificate proves the donation was made without revealing who made it\n\nThe certificate also records the donation amount and date, so applications can make trust decisions based on how much someone invested and when -- without knowing who they are.\n\n## Integrating with ghostkeys\n\nIf you're building a Freenet application and want to verify users or prevent spam, you interact with the ghostkeys delegate through Freenet's delegate messaging API. Your app sends a `GhostkeyRequest`, the user is prompted for permission, and you receive a `GhostkeyResponse`.\n\n### Requesting a signature\n\nYour application sends a `SignMessage` request specifying which ghost key to use (by fingerprint) and the message to sign:\n\n```rust\nuse ghostkey_common::{GhostkeyRequest, to_cbor};\n\nlet request = GhostkeyRequest::SignMessage {\n    fingerprint: \"abc123\".into(),\n    message: b\"hello world\".to_vec(),\n};\n\n// Send via Freenet's delegate messaging API\nlet payload = to_cbor(\u0026request).unwrap();\n// ... send as DelegateMessage or ApplicationMessage to the ghostkeys delegate\n```\n\nThe delegate checks whether your application has permission. If this is the first time your app has requested access to this key, the user sees a prompt in their browser:\n\n\u003e \"Web app DLog47h... wants to access ghostkey abc123. Allow?\"\n\nThe user can choose **allow once**, **always allow**, or **deny**. If they choose \"allow once\", the permission is automatically revoked after the request completes.\n\nOn approval, the delegate signs the message and returns a `SignResult`:\n\n```rust\nuse ghostkey_common::GhostkeyResponse;\n\n// The response you receive:\nGhostkeyResponse::SignResult {\n    scoped_payload,   // CBOR-serialized ScopedPayload (message + caller identity)\n    signature,        // Ed25519 signature over scoped_payload\n    certificate_pem,  // Full certificate chain for verification\n}\n```\n\n### Verifying a signature\n\nAny party can verify a signature -- they don't need access to the delegate. Given the signed bundle, verification checks the Ed25519 signature, validates the certificate chain back to Freenet's master key, and extracts the original message along with metadata:\n\n```rust\nlet request = GhostkeyRequest::VerifySignedMessage {\n    signed_message: bundle_bytes,\n};\n\n// Response:\nGhostkeyResponse::VerifyResult {\n    valid: true,\n    signer_fingerprint: Some(\"abc123\".into()),\n    delegate_info: Some(\"{\\\"action\\\":\\\"freenet-donation\\\",\\\"amount\\\":20,...}\".into()),\n    requestor: Some(SignatureRequestor::WebApp(contract_id)),\n    message: Some(original_message_bytes),\n}\n```\n\nThe `delegate_info` field contains the donation tier, so a verifier can see \"this was signed by someone who donated $20\" without learning anything about who that person is.\n\n### Scoped signatures\n\nThe delegate never signs raw messages. Every signature wraps the message in a `ScopedPayload` that includes the runtime-attested identity of the requesting application:\n\n```rust\npub struct ScopedPayload {\n    pub requestor: SignatureRequestor,  // Which app/delegate requested this\n    pub payload: Vec\u003cu8\u003e,              // The actual message\n}\n```\n\nThis is a deliberate design choice. If App A obtains a signature, that signature includes App A's identity in the signed data. App B cannot strip that out and claim the signature was made for it -- the signature verification would fail. This prevents signature harvesting: a malicious app can't collect signatures and replay them in a different context.\n\nThe `SignatureRequestor` is attested by Freenet's runtime, not self-reported by the caller:\n\n```rust\npub enum SignatureRequestor {\n    WebApp(ContractInstanceId),  // A web app, identified by its contract\n    Delegate(DelegateKey),       // Another delegate on the same node\n}\n```\n\n### Permission model in practice\n\nNot all operations require permission. Importing a key and listing keys are open; the sensitive operations -- signing, exporting, reading certificate details, deleting -- require explicit user consent.\n\nThe flow when an unpermitted app makes a request:\n\n1. App sends `SignMessage` to the ghostkeys delegate\n2. Delegate checks permissions, finds none for this app + key combination\n3. Delegate stores the pending request and emits a `RequestUserInput` prompt\n4. User sees the prompt in their browser with three options: allow / always allow / deny\n5. On approval, the delegate replays the original request with permission now granted\n6. If \"allow once\" was chosen, the permission is revoked immediately after the response is sent\n\nThis means your application doesn't need to handle the permission flow at all -- it just sends the request and eventually gets back either a `SignResult` or a `PermissionDenied`. The delegate and Freenet's runtime handle the user interaction transparently.\n\n## Architecture\n\nThis is a Rust workspace with three crates:\n\n- **`common/`** -- Shared types and CBOR serialization for the request/response protocol between delegate and UI\n- **`delegates/ghostkey-delegate/`** -- The Freenet delegate (compiles to WASM) that stores signing keys and handles identity operations\n- **`ui/`** -- Dioxus web frontend for managing identities, signing messages, and granting permissions\n\n### Certificate chain\n\n```\nMaster Key -\u003e Notary Certificate -\u003e Ghost Key Certificate\n```\n\nEach ghost key is identified by a fingerprint (first 8 bytes of BLAKE3 hash of the verifying key, base58-encoded).\n\n### Operations\n\n| Request | Description |\n|---------|-------------|\n| `ImportGhostKey` | Store a new ghost key from PEM certificate + signing key |\n| `ListGhostKeys` | List ghost keys the caller has access to |\n| `GetGhostKey` | Get full certificate details for a key |\n| `GetCertificate` | Get just the public certificate (for sharing) |\n| `SignMessage` | Sign a message with a ghost key (scoped to caller) |\n| `VerifySignedMessage` | Verify a signed message and extract metadata |\n| `ExportGhostKey` | Export certificate + private signing key for backup |\n| `DeleteGhostKey` | Remove a stored ghost key |\n| `SetLabel` | Set a user-friendly name for a key |\n| `GrantPermission` | Grant another app/delegate access to a key |\n| `RevokePermission` | Revoke access |\n| `ListPermissions` | List who has access to a key |\n\n### Delegate UI\n\nWhen running a Freenet peer locally, the ghost key management UI is available at:\n\n```\nhttp://127.0.0.1:7509/v1/contract/web/DLog47hEsrtuGT4N5XCeMBG45m4n1aWM89tBZXue2E1N/\n```\n\n## Building\n\nRequires [cargo-make](https://github.com/sagiegurari/cargo-make):\n\n```bash\ncargo install cargo-make\n```\n\nBuild the delegate (WASM):\n```bash\ncargo make build-delegate\n```\n\nBuild the UI:\n```bash\ncargo make build-ui\n```\n\nRun the dev server with hot reload:\n```bash\ncargo make dev\n```\n\nRun tests:\n```bash\ncargo make test\n```\n\n## Ghost Key CLI\n\nA separate [command-line tool](https://crates.io/crates/ghostkey) is available for generating, verifying, and using ghost keys outside of Freenet:\n\n```\ncargo install ghostkey\nghostkey -h\n```\n\nThe CLI is part of the [freenet/web](https://github.com/freenet/web) repository (`rust/cli/`).\n\n## Dependencies\n\n| Crate | Purpose |\n|-------|---------|\n| [freenet-stdlib](https://crates.io/crates/freenet-stdlib) | Delegate framework and WebSocket API |\n| [ghostkey_lib](https://crates.io/crates/ghostkey_lib) | Certificate and signing key serialization |\n| [ed25519-dalek](https://crates.io/crates/ed25519-dalek) | Ed25519 signing and verification |\n| [dioxus](https://dioxuslabs.com/) | Web UI framework (WASM target) |\n| [ciborium](https://crates.io/crates/ciborium) | CBOR wire protocol encoding |\n| [blake3](https://crates.io/crates/blake3) | Key fingerprinting |\n\n## License\n\nMIT OR Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreenet%2Fghostkeys","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffreenet%2Fghostkeys","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreenet%2Fghostkeys/lists"}