1.5.0 update
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
PublicKey ed25519.PublicKey
|
||||
PrivateKey ed25519.PrivateKey
|
||||
Fingerprint string // hex SHA256 of public key
|
||||
}
|
||||
|
||||
func DeriveIdentity() Identity {
|
||||
hwid := deriveHWID()
|
||||
machineID := platformMachineID()
|
||||
if machineID == "" {
|
||||
log.Printf("[identity] WARNING: no OS-specific machine ID available, using HWID-only derivation")
|
||||
machineID = hwid
|
||||
}
|
||||
|
||||
hkdfReader := hkdf.New(sha256.New, []byte(machineID), []byte(hwid), []byte("overlord-identity"))
|
||||
seed := make([]byte, ed25519.SeedSize)
|
||||
if _, err := io.ReadFull(hkdfReader, seed); err != nil {
|
||||
log.Fatalf("[identity] HKDF derivation failed: %v", err)
|
||||
}
|
||||
|
||||
privateKey := ed25519.NewKeyFromSeed(seed)
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
|
||||
fp := sha256.Sum256(publicKey)
|
||||
|
||||
return Identity{
|
||||
PublicKey: publicKey,
|
||||
PrivateKey: privateKey,
|
||||
Fingerprint: hex.EncodeToString(fp[:]),
|
||||
}
|
||||
}
|
||||
|
||||
func (id *Identity) Sign(data []byte) []byte {
|
||||
return ed25519.Sign(id.PrivateKey, data)
|
||||
}
|
||||
|
||||
func (id *Identity) PublicKeyBase64() string {
|
||||
return base64.StdEncoding.EncodeToString(id.PublicKey)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build android
|
||||
|
||||
package config
|
||||
|
||||
func platformMachineID() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build darwin
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func platformMachineID() string {
|
||||
out, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
|
||||
if err != nil {
|
||||
log.Printf("[identity] WARNING: failed to run ioreg: %v", err)
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.Contains(line, "IOPlatformUUID") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
return strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[identity] WARNING: IOPlatformUUID not found in ioreg output")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func platformMachineID() string {
|
||||
data, err := os.ReadFile("/etc/machine-id")
|
||||
if err != nil {
|
||||
log.Printf("[identity] WARNING: failed to read /etc/machine-id: %v", err)
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build windows
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func platformMachineID() string {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
log.Printf("[identity] WARNING: failed to open Cryptography registry key: %v", err)
|
||||
return ""
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
val, _, err := k.GetStringValue("MachineGuid")
|
||||
if err != nil {
|
||||
log.Printf("[identity] WARNING: failed to read MachineGuid: %v", err)
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -134,6 +135,12 @@ func runClient(cfg config.Config) {
|
||||
sleepFor = reconnectDelay()
|
||||
log.Printf("reconnect: immediate retry in %s", sleepFor)
|
||||
}
|
||||
if sessionErr != nil {
|
||||
if d := enrollmentRetryDelay(sessionErr); d > 0 {
|
||||
sleepFor = d
|
||||
log.Printf("purgatory: retry in %s", sleepFor)
|
||||
}
|
||||
}
|
||||
time.Sleep(sleepFor)
|
||||
}
|
||||
}
|
||||
@@ -304,6 +311,9 @@ func shouldRetryImmediately(err error) bool {
|
||||
}
|
||||
var closeErr *websocket.CloseError
|
||||
if errors.As(err, &closeErr) {
|
||||
if closeErr.Code == 4001 || closeErr.Code == 4002 || closeErr.Code == 4003 {
|
||||
return false
|
||||
}
|
||||
if closeErr.Code == websocket.StatusNormalClosure || closeErr.Code == websocket.StatusGoingAway {
|
||||
return true
|
||||
}
|
||||
@@ -321,6 +331,43 @@ func shouldRetryImmediately(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func enrollmentRetryDelay(err error) time.Duration {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "purgatory: status=pending") {
|
||||
return getEnrollmentRetryInterval()
|
||||
}
|
||||
if strings.Contains(msg, "purgatory: status=denied") {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
|
||||
var closeErr *websocket.CloseError
|
||||
if errors.As(err, &closeErr) {
|
||||
switch closeErr.Code {
|
||||
case 4001: // pending
|
||||
return getEnrollmentRetryInterval()
|
||||
case 4002: // invalid signature
|
||||
return 60 * time.Second
|
||||
case 4003: // denied
|
||||
return 5 * time.Minute
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getEnrollmentRetryInterval() time.Duration {
|
||||
raw := strings.TrimSpace(os.Getenv("OVERLORD_ENROLLMENT_RETRY_MS"))
|
||||
if raw != "" {
|
||||
if ms, err := strconv.Atoi(raw); err == nil && ms > 0 {
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
}
|
||||
return 30 * time.Second
|
||||
}
|
||||
|
||||
func getPingInterval() time.Duration {
|
||||
raw := strings.TrimSpace(os.Getenv("OVERLORD_PING_INTERVAL_MS"))
|
||||
if raw == "" {
|
||||
@@ -350,7 +397,50 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
|
||||
defer cancel()
|
||||
defer conn.Close(websocket.StatusNormalClosure, "bye")
|
||||
|
||||
identity := config.DeriveIdentity()
|
||||
log.Printf("[purgatory] identity fingerprint=%s", identity.Fingerprint)
|
||||
|
||||
challengeCtx, challengeCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer challengeCancel()
|
||||
|
||||
_, challengeData, err := conn.Read(challengeCtx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purgatory: failed to read challenge: %w", err)
|
||||
}
|
||||
challengeEnvelope, err := wire.DecodeEnvelope(challengeData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purgatory: failed to decode challenge: %w", err)
|
||||
}
|
||||
|
||||
safeWriter := wire.NewSafeWriter(conn)
|
||||
|
||||
msgType, _ := challengeEnvelope["type"].(string)
|
||||
var publicKeyB64, signatureB64 string
|
||||
|
||||
switch msgType {
|
||||
case "enrollment_challenge":
|
||||
nonceB64, _ := challengeEnvelope["nonce"].(string)
|
||||
if nonceB64 == "" {
|
||||
return fmt.Errorf("purgatory: empty nonce in challenge")
|
||||
}
|
||||
nonceBytes, decErr := base64.StdEncoding.DecodeString(nonceB64)
|
||||
if decErr != nil {
|
||||
return fmt.Errorf("purgatory: invalid nonce base64: %w", decErr)
|
||||
}
|
||||
sig := identity.Sign(nonceBytes)
|
||||
publicKeyB64 = identity.PublicKeyBase64()
|
||||
signatureB64 = base64.StdEncoding.EncodeToString(sig)
|
||||
log.Printf("[purgatory] signed challenge nonce (%d bytes)", len(nonceBytes))
|
||||
|
||||
case "hello_ack":
|
||||
log.Printf("[purgatory] legacy server (no challenge), proceeding")
|
||||
publicKeyB64 = ""
|
||||
signatureB64 = ""
|
||||
|
||||
default:
|
||||
return fmt.Errorf("purgatory: unexpected first message type: %s", msgType)
|
||||
}
|
||||
|
||||
env := &rt.Env{Conn: safeWriter, Cfg: cfg, Cancel: cancel, SelectedDisplay: handlers.GetPersistedDisplay()}
|
||||
env.SetLastPong(time.Now().UnixMilli())
|
||||
env.Console = rt.NewConsoleHub(env)
|
||||
@@ -389,12 +479,45 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
|
||||
MonitorInfo: toWireMonitorInfo(capture.MonitorInfos()),
|
||||
Country: cfg.Country,
|
||||
BuildTag: cfg.BuildTag,
|
||||
PublicKey: publicKeyB64,
|
||||
Signature: signatureB64,
|
||||
}
|
||||
|
||||
if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil {
|
||||
return fmt.Errorf("send hello: %w", err)
|
||||
}
|
||||
|
||||
if msgType == "enrollment_challenge" {
|
||||
ackCtx, ackCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer ackCancel()
|
||||
|
||||
for {
|
||||
_, ackData, err := conn.Read(ackCtx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purgatory: failed to read response: %w", err)
|
||||
}
|
||||
ackEnvelope, err := wire.DecodeEnvelope(ackData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purgatory: failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
ackType, _ := ackEnvelope["type"].(string)
|
||||
switch ackType {
|
||||
case "hello_ack":
|
||||
log.Printf("[purgatory] approved, proceeding with session")
|
||||
case "enrollment_status":
|
||||
status, _ := ackEnvelope["status"].(string)
|
||||
log.Printf("[purgatory] server returned status=%s", status)
|
||||
return fmt.Errorf("purgatory: status=%s", status)
|
||||
case "ping", "pong":
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("purgatory: unexpected response type: %s", ackType)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := wire.WriteMsg(ctx, env.Conn, wire.Ping{Type: "ping", TS: time.Now().UnixMilli()}); err != nil {
|
||||
log.Printf("ping: failed to send initial ping: %v", err)
|
||||
cancel()
|
||||
|
||||
@@ -13,6 +13,18 @@ type Hello struct {
|
||||
MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"`
|
||||
Country string `msgpack:"country,omitempty"`
|
||||
BuildTag string `msgpack:"buildTag,omitempty"`
|
||||
PublicKey string `msgpack:"publicKey,omitempty"`
|
||||
Signature string `msgpack:"signature,omitempty"`
|
||||
}
|
||||
|
||||
type EnrollmentChallenge struct {
|
||||
Type string `msgpack:"type"`
|
||||
Nonce string `msgpack:"nonce"`
|
||||
}
|
||||
|
||||
type EnrollmentStatus struct {
|
||||
Type string `msgpack:"type"`
|
||||
Status string `msgpack:"status"`
|
||||
}
|
||||
|
||||
type MonitorInfo struct {
|
||||
|
||||
Reference in New Issue
Block a user