Initial push
This commit is contained in:
285
G1WALLET-SPEC.md
Normal file
285
G1WALLET-SPEC.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_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
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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.
|
||||||
Reference in New Issue
Block a user