Files
kane-diagnostics/G1WALLET-SPEC.md
2026-06-08 02:42:59 -04:00

12 KiB

G1WALLET — Self-Sovereign Ğ1 Wallet Specification

Project: kane-diagnostics
Version: 1.0
Prerequisite reading: README.md, ADDON-SKELETON-SPEC.md, ASSOCIATION-CHANNEL-MODEL.md, CRY01-SPEC.md, FOR-PARTICIPANTS-AND-PROFESSIONALS.md


Principle

The operator is not privileged in the value layer. Every participant — including the operator — holds their own Ğ1 identity under their own credentials, in their own browser, under their own control. The Civic Infrastructure provides the wallet interface. It never touches the keys.

This principle is non-negotiable. Any architecture that requires the platform to hold, proxy, or intermediate participant funds is incompatible with the Civic Infrastructure's design. The platform is a record system and a credential layer. It is not a bank.


Purpose

g1wallet is a self-sovereign Ğ1 wallet embedded in the Civic Infrastructure. It provides every SASE-verified participant — including the operator — with:

  1. Identity — a Ğ1 keypair derived from credentials the participant controls, in the participant's browser, never transmitted to the server
  2. Balance — the participant's current Ğ1 balance, read from the local Duniter node via the orchestrator
  3. Signing — the ability to sign Duniter transactions (transfers, certifications) in the browser, with the signed transaction broadcast to the Duniter network via the orchestrator
  4. Integration — the wallet session provides the participant's Ğ1 public key to other addons (cry01, poll01) automatically — participants never type their public key manually

Self-Custody Model

The keypair derivation algorithm is scrypt + Ed25519 — the same algorithm used by Cesium and Ğecko. It is a documented open standard. The same credentials always produce the same keypair, on any device, in any browser.

What this means:

  • The private key is derived in the browser from the participant's pseudo and password
  • The private key exists only in browser memory for the duration of the wallet session
  • On page close, the private key is gone
  • The server never sees the private key — not in a POST, not in a cookie, not in a log
  • The public key is the only thing the server ever knows about
  • If the participant loses their credentials, the key is unrecoverable — no reset, no recovery

What the Civic Infrastructure stores:

  • The participant's Ğ1 public key — stored in their Hubzilla channel settings after first wallet unlock
  • Nothing else related to the wallet

The Operator's Wallet

The operator's g1wallet session is identical to any participant's. The operator does not have a privileged key. The operator's Ğ1 public key is stored in their channel settings, not in config.json.

The only operator-specific wallet function is Ğ1 certification signing — the operator signs certification documents for SASE participants who are eligible for web of trust certification. This signing happens in the operator's browser during an active wallet session. The signed document is then broadcast to the Duniter network via the orchestrator. The private key never leaves the browser.


Architecture

Client-Side (JavaScript)

All key operations happen in the browser using the WebCrypto API and a vendored scrypt implementation.

Key derivation:

pseudo + password → scrypt(N=4096, r=16, p=1) → 32-byte seed → Ed25519 keypair

The scrypt parameters match the Duniter/Cesium standard exactly. A keypair derived in g1wallet is identical to the same keypair derived in Cesium or Ğecko from the same credentials.

Vendored library: vendor/scrypt-js-3.0.1.min.js — pinned version, included in the repo, no CDN dependency.

Signing: The browser's native SubtleCrypto.sign() with the Ed25519 algorithm signs Duniter transaction documents. The signed bytes are base64-encoded and sent to the orchestrator for broadcast. The private key object is marked non-extractable in the WebCrypto API — it cannot be read back out of memory by JavaScript.

Server-Side (PHP)

The PHP layer:

  • Renders the wallet unlock form and wallet interface
  • Receives the participant's public key after unlock and stores it in Hubzilla channel settings
  • Provides a session token that confirms to other addons that the wallet is unlocked
  • Never receives, stores, or processes the private key or the credentials

Orchestrator

The orchestrator:

  • Accepts signed Duniter transaction documents from the browser (via the Hubzilla addon) and broadcasts them to the Duniter network
  • Returns balance information for a given public key
  • Never generates keys, never holds keys, never signs on behalf of any participant

Wallet Session

A wallet session begins when the participant successfully derives their keypair in the browser. The session is held in browser memory only — not in a PHP session variable, not in localStorage, not in a cookie.

The wallet session provides:

  • The participant's Ğ1 public key (safe to expose — it is public)
  • A JavaScript API that other addons can call to request a signature

When a participant navigates to the cry01 signal form with an active wallet session, the form's g1_pubkey field is populated automatically by the wallet session. The participant never types their public key.

When a participant navigates away from the Hubzilla page, the session ends and the private key is gone.


Wallet States

LOCKED → UNLOCKED → SIGNING

LOCKED — no active session. The wallet unlock form is displayed. The participant enters their pseudo and password.

UNLOCKED — keypair is in browser memory. Public key is displayed. Balance is shown. Signing operations are available.

SIGNING — a signing request has been initiated (by cry01, poll01, or the operator certification flow). The participant reviews the document to be signed and confirms or rejects.


Route Structure

/g1wallet/                     — wallet landing: unlock form or unlocked interface
/g1wallet/balance              — balance refresh for the current unlocked public key
/g1wallet/broadcast            — POST: receive signed transaction from browser, broadcast to Duniter
/g1wallet/pubkey               — POST: store public key in channel settings after unlock

Access Model

Route Public Participant Professional Operator
Wallet unlock form wall
Unlocked interface
Balance
Broadcast
Pubkey store

The wallet is not publicly accessible. A participant must complete SASE before accessing their wallet through the Civic Infrastructure. They can always use Cesium or Ğecko directly — the Civic Infrastructure wallet is a convenience layer, not the only access path.


Integration with cry01 and poll01

g1wallet exposes a JavaScript event API that other addons listen to:

// cry01 signal form listens for wallet unlock
window.addEventListener('g1wallet:unlocked', function(e) {
    document.getElementById('g1_pubkey').value = e.detail.pubkey;
});

// poll01 ballot form listens for wallet unlock
window.addEventListener('g1wallet:unlocked', function(e) {
    document.getElementById('voter_pubkey').value = e.detail.pubkey;
});

// cry01 requests a signature for a capacity signal
window.dispatchEvent(new CustomEvent('g1wallet:sign_request', {
    detail: {
        document: signableDocument,
        callback: 'cry01_signal_signed'
    }
}));

This means:

  • A participant who has unlocked their wallet can submit a signal or cast a ballot without typing their public key
  • A participant who has not unlocked their wallet is prompted to do so before the form is submitted
  • The wallet session is the single source of identity for all value-layer operations

Operator Certification Signing

When the operator visits /cry01/[slug]/g1 (the certification candidate list), and a candidate is ready to certify, the operator:

  1. Unlocks their wallet (if not already unlocked)
  2. Reviews the attestation document for the candidate
  3. Clicks "Certify" — the wallet signs the Duniter certification document in the browser
  4. The signed document is POSTed to /g1wallet/broadcast
  5. The orchestrator broadcasts it to the Duniter network
  6. The candidate's Ğ1 certification is recorded on-chain

The operator's private key signs exactly one document per click. The operator reviews the document before signing. There is no batch signing.


Skeleton File Structure

hubzilla/addon/g1wallet/
  g1wallet.php               — entry point: hooks, routing, access state
  g1wallet_renderer.php      — unlock form, unlocked interface, balance display
  g1wallet_spool.php         — POST handlers: pubkey store, broadcast relay
  g1wallet.apd               — app descriptor
  mod_g1wallet.pdl           — PDL layout
  config.json.template       — orchestrator endpoint only
  Widget/
    G1wallet.php             — sidebar widget (standard pattern)
  view/
    css/
      g1wallet.css
    js/
      g1wallet.js            — key derivation, signing, session management, event API
  vendor/
    scrypt-js-3.0.1.min.js   — vendored scrypt implementation, pinned version
  README.md

Config Template Fields

{
  "_note": "Copy to config.json. Do not commit config.json.",
  "orchestrator_url": "REPLACE — orchestrator base URL, e.g. http://10.0.0.105:8700",
  "node_token": "REPLACE — shared secret for node-to-orchestrator auth",
  "g1_rpc_endpoint": "REPLACE — Duniter node RPC over Wireguard, e.g. http://10.0.0.105:9944",
  "listings_file": "REPLACE — absolute path to listings.json on the host",
  "directory_default_tab": "core"
}

Note: No public key, no private key, no credentials of any kind in config.


Orchestrator Additions

Two new endpoints added to main.py on the orchestrator:

POST /g1wallet/broadcast — receives a signed Duniter transaction document (base64-encoded), validates the node token, and broadcasts it to the Duniter network via the local RPC endpoint. Returns the transaction hash on success.

GET /g1wallet/balance/{pubkey} — queries the Duniter node for the balance of the given public key. Returns the balance as a JSON object. The public key is in the URL — it is public information.


civicnav.json Entry

{
  "id": "g1wallet",
  "label": "Ğ1 Wallet",
  "module": "g1wallet",
  "icon": "wallet2",
  "enabled": true
}

Changes Required to cry01

  1. Remove operator_g1_pubkey and operator_did from config.json.template
  2. Remove operator_g1_pubkey from cry01_chain.php — balance cache is refreshed using the key from the operator's wallet session, passed as a POST parameter to the manage route
  3. Signal form g1_pubkey field becomes read-only, populated by the wallet session event
  4. Signal form shows a wallet unlock prompt if no wallet session is active

Build Sequence

g1wallet must be built before cry01 signal submission and poll01 ballot submission are functional. It is a dependency of both.

  1. g1wallet.js — key derivation, signing, session management, event API. This is the most critical file. It must be tested against the Duniter standard key derivation before any PHP is written.
  2. vendor/scrypt-js-3.0.1.min.js — download and pin before writing g1wallet.js
  3. g1wallet.php — entry point, routing, access state
  4. g1wallet_renderer.php — unlock form, unlocked interface
  5. g1wallet_spool.php — pubkey store, broadcast relay
  6. Orchestrator additions — broadcast and balance endpoints in main.py
  7. Widget, PDL, CSS
  8. Integration test: derive keypair, verify public key matches Cesium for same credentials, sign a test document, broadcast to Duniter testnet

What Not To Do

  • Do not send the private key to the server. Ever. Under any circumstance.
  • Do not send the password or pseudo to the server. Ever.
  • Do not store the private key in localStorage, sessionStorage, or any browser persistent storage.
  • Do not use a CDN for the scrypt library — vendor it with a pinned version.
  • Do not implement batch signing — one document, one confirmation, one click.
  • Do not allow the wallet to auto-sign without explicit participant confirmation of the document being signed.
  • Do not exceed 500 lines in any PHP file.
  • Do not push from the host. Gitea is the SSOT.