go-p2p/docs/identity.md

145 lines
4.7 KiB
Markdown
Raw Permalink Normal View History

---
title: Node Identity
description: X25519 keypair generation, node ID derivation, and HMAC-SHA256 challenge-response authentication.
---
# Node Identity
Every node in the mesh has a unique identity derived from an X25519 keypair. The node ID is cryptographically bound to the public key, and authentication uses HMAC-SHA256 challenge-response over a shared secret derived via ECDH.
## NodeIdentity
The public identity struct carried in handshake messages and stored on disk:
```go
type NodeIdentity struct {
ID string `json:"id"` // 32-char hex, derived from public key
Name string `json:"name"` // Human-friendly name
PublicKey string `json:"publicKey"` // X25519 base64
CreatedAt time.Time `json:"createdAt"`
Role NodeRole `json:"role"` // controller, worker, or dual
}
```
The `ID` is computed as the first 16 bytes of `SHA-256(publicKey)`, hex-encoded to produce a 32-character string.
## Key Storage
| Item | Path | Permissions |
|------|------|-------------|
| Private key | `~/.local/share/lethean-desktop/node/private.key` | `0600` |
| Identity config | `~/.config/lethean-desktop/node.json` | `0644` |
Paths follow XDG base directories via `github.com/adrg/xdg`. The private key is never serialised to JSON or transmitted over the network.
## NodeManager
`NodeManager` handles identity lifecycle -- generation, persistence, loading, and deletion. It also derives shared secrets for peer authentication.
### Creating an Identity
```go
nm, err := node.NewNodeManager()
if err != nil {
log.Fatal(err)
}
// Generate a new identity (persists key and config to disk)
err = nm.GenerateIdentity("eu-controller-01", node.RoleController)
```
Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce the X25519 keypair.
### Custom Paths (Testing)
```go
nm, err := node.NewNodeManagerWithPaths(
"/tmp/test/private.key",
"/tmp/test/node.json",
)
```
### Checking and Retrieving Identity
```go
if nm.HasIdentity() {
identity := nm.GetIdentity() // Returns a copy
fmt.Println(identity.ID, identity.Name)
}
```
`GetIdentity()` returns a copy of the identity struct to prevent mutation of the internal state.
### Deriving Shared Secrets
```go
sharedSecret, err := nm.DeriveSharedSecret(peerPublicKeyBase64)
```
This performs X25519 ECDH with the peer's public key and hashes the result with SHA-256, producing a 32-byte symmetric key. The same shared secret is derived independently by both sides (no secret is transmitted).
### Deleting an Identity
```go
err := nm.Delete() // Removes key and config from disk, clears in-memory state
```
## Challenge-Response Authentication
After the ECDH key exchange, nodes prove identity possession through HMAC-SHA256 challenge-response. The shared secret is never transmitted.
### Functions
```go
// Generate a 32-byte cryptographically random challenge
challenge, err := node.GenerateChallenge()
// Sign a challenge with the shared secret (HMAC-SHA256)
response := node.SignChallenge(challenge, sharedSecret)
// Verify a challenge response (constant-time comparison via hmac.Equal)
ok := node.VerifyChallenge(challenge, response, sharedSecret)
```
### Authentication Flow
```
Node A (initiator) Node B (responder)
| |
|--- handshake (identity + challenge) ->|
| |
| [B derives shared secret via ECDH] |
| [B checks protocol version] |
| [B checks allowlist] |
| [B signs challenge with HMAC] |
| |
|<-- handshake_ack (identity + sig) ---|
| |
| [A derives shared secret via ECDH] |
| [A verifies challenge response] |
| |
| ---- encrypted channel open ---- |
```
The handshake and handshake_ack messages are sent unencrypted (they carry the public keys needed to derive the shared secret). All subsequent messages are SMSG-encrypted.
## Node Roles
```go
const (
RoleController NodeRole = "controller" // Manages the mesh, distributes tasks
RoleWorker NodeRole = "worker" // Receives and executes workloads
RoleDual NodeRole = "dual" // Both controller and worker
)
```
| Role | Behaviour |
|------|-----------|
| `controller` | Sends `get_stats`, `start_miner`, `stop_miner`, `get_logs`, `deploy` messages |
| `worker` | Handles incoming commands, runs compute tasks, reports stats |
| `dual` | Participates as both controller and worker |
## Thread Safety
`NodeManager` is safe for concurrent use. A `sync.RWMutex` protects all internal state. `GetIdentity()` returns a copy rather than a pointer to the internal struct.