145 lines
4.7 KiB
Markdown
145 lines
4.7 KiB
Markdown
|
|
---
|
||
|
|
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.
|