From 2592a2613b1c10c969acc6415d555359c0b9035f Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Sun, 21 Jun 2026 04:51:43 +0300 Subject: [PATCH] Add driver SubmitBadge and EnrollRecovery client methods --- driver/driver.go | 27 ++++++++++ driver/ipc.go | 11 +++- driver/zz_driver_simple_ops_test.go | 81 +++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/driver/driver.go b/driver/driver.go index 8eca2d4..c9fba5d 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -361,6 +361,33 @@ func (d *Driver) RotateKey() (map[string]interface{}, error) { return d.jsonRPC([]byte{cmdRotateKey}, cmdRotateKeyOK, "rotate_key") } +// SubmitBadge attaches a verified-address badge to this node's registry +// entry. badge and badgeSig are produced out-of-band by the verifier +// sidecar; the daemon signs proof of the current key over the badge before +// forwarding to the registry, which also verifies the badge offline against +// the pinned issuer key. Verification is optional — nodes without a badge +// keep working unchanged. +func (d *Driver) SubmitBadge(badge, badgeSig string) (map[string]interface{}, error) { + data, _ := json.Marshal(map[string]string{"badge": badge, "badge_sig": badgeSig}) + msg := make([]byte, 1+len(data)) + msg[0] = cmdSubmitBadge + copy(msg[1:], data) + return d.jsonRPC(msg, cmdSubmitBadgeOK, "submit_badge") +} + +// EnrollRecovery records this node's opaque recovery commitment so the +// address can later be recovered if the current key is lost. enrollment and +// enrollmentSig come from the verifier sidecar; the daemon signs proof of the +// current key over the commitment before forwarding to the registry. The +// raw external identity never leaves the verifier — only the commitment. +func (d *Driver) EnrollRecovery(enrollment, enrollmentSig string) (map[string]interface{}, error) { + data, _ := json.Marshal(map[string]string{"enrollment": enrollment, "enrollment_sig": enrollmentSig}) + msg := make([]byte, 1+len(data)) + msg[0] = cmdEnrollRecovery + copy(msg[1:], data) + return d.jsonRPC(msg, cmdEnrollRecoveryOK, "enroll_recovery") +} + // Disconnect closes a connection by ID. Used by administrative tools. // Fire-and-forget: the daemon always responds CmdCloseOK regardless of // whether the connID exists, so there is no error to propagate. Using diff --git a/driver/ipc.go b/driver/ipc.go index 38cd06d..c2a88fc 100644 --- a/driver/ipc.go +++ b/driver/ipc.go @@ -60,6 +60,15 @@ const ( // traffic (send-file) is failing while small messages (ping) work. cmdPreferDirect byte = 0x2D cmdPreferDirectOK byte = 0x2E + // cmdSubmitBadge attaches a verified-address badge to this node. The + // daemon adds a signature by the current key proving ownership before + // forwarding to the registry. cmdEnrollRecovery records the node's + // opaque recovery commitment the same way. Both are optional features: + // older daemons without these handlers reply cmdError. + cmdSubmitBadge byte = 0x2F + cmdSubmitBadgeOK byte = 0x30 + cmdEnrollRecovery byte = 0x31 + cmdEnrollRecoveryOK byte = 0x32 ) // Network sub-commands (must match daemon SubNetwork* constants) @@ -192,7 +201,7 @@ func (c *ipcClient) readLoop() { cmdResolveHostnameOK, cmdSetHostnameOK, cmdSetVisibilityOK, cmdDeregisterOK, cmdSetTagsOK, cmdSetWebhookOK, cmdNetworkOK, cmdHealthOK, cmdManagedOK, cmdRotateKeyOK, cmdBroadcastOK, - cmdPreferDirectOK: + cmdPreferDirectOK, cmdSubmitBadgeOK, cmdEnrollRecoveryOK: // Known response cmds: route to pending for the in-flight sendAndWait. select { case c.pending <- &pendingResponse{cmd: cmd, payload: append([]byte(nil), payload...)}: diff --git a/driver/zz_driver_simple_ops_test.go b/driver/zz_driver_simple_ops_test.go index cf0832e..b310ba2 100644 --- a/driver/zz_driver_simple_ops_test.go +++ b/driver/zz_driver_simple_ops_test.go @@ -4,6 +4,7 @@ package driver import ( "encoding/binary" + "encoding/json" "testing" ) @@ -182,3 +183,83 @@ func TestDriverRotateKey(t *testing.T) { t.Errorf("RotateKey result is nil") } } + +// TestDriverSubmitBadge covers SubmitBadge's JSON-RPC roundtrip: the request +// frame is [cmdSubmitBadge][JSON{badge,badge_sig}] and the cmdSubmitBadgeOK +// reply is routed and unmarshalled. A new response opcode that readLoop does +// not allowlist would silently hang here — this pins that wiring. +func TestDriverSubmitBadge(t *testing.T) { + t.Parallel() + d := newFakeDaemon(t) + defer d.close() + + const wantBadge = "pilotbadge:v1:109517:github:1781827200:0:bdg-v1:" + const wantSig = "ZmFrZS1zaWc=" + + d.onCmd(cmdSubmitBadge, func(frame []byte) [][]byte { + if frame[0] != cmdSubmitBadge { + t.Errorf("opcode = 0x%02X, want 0x%02X", frame[0], cmdSubmitBadge) + } + var got map[string]string + if err := json.Unmarshal(frame[1:], &got); err != nil { + t.Errorf("payload not JSON: %v", err) + return [][]byte{{cmdError, 'b', 'a', 'd'}} + } + if got["badge"] != wantBadge || got["badge_sig"] != wantSig { + t.Errorf("payload = %+v, want badge=%q sig=%q", got, wantBadge, wantSig) + } + body := []byte(`{"ok":true}`) + return [][]byte{append([]byte{cmdSubmitBadgeOK}, body...)} + }) + + drv, err := Connect(d.path) + if err != nil { + t.Fatalf("Connect: %v", err) + } + defer drv.Close() + + result, err := drv.SubmitBadge(wantBadge, wantSig) + if err != nil { + t.Fatalf("SubmitBadge: %v", err) + } + if ok, _ := result["ok"].(bool); !ok { + t.Errorf("result = %+v, want ok=true", result) + } +} + +// TestDriverEnrollRecovery covers EnrollRecovery's JSON-RPC roundtrip. +func TestDriverEnrollRecovery(t *testing.T) { + t.Parallel() + d := newFakeDaemon(t) + defer d.close() + + const wantEnroll = "pilotenroll:v1:109517:github:Y29tbWl0:1781827200:bdg-v1" + const wantSig = "ZW5yb2xsLXNpZw==" + + d.onCmd(cmdEnrollRecovery, func(frame []byte) [][]byte { + var got map[string]string + if err := json.Unmarshal(frame[1:], &got); err != nil { + t.Errorf("payload not JSON: %v", err) + return [][]byte{{cmdError, 'b', 'a', 'd'}} + } + if got["enrollment"] != wantEnroll || got["enrollment_sig"] != wantSig { + t.Errorf("payload = %+v, want enrollment=%q sig=%q", got, wantEnroll, wantSig) + } + body := []byte(`{"ok":true}`) + return [][]byte{append([]byte{cmdEnrollRecoveryOK}, body...)} + }) + + drv, err := Connect(d.path) + if err != nil { + t.Fatalf("Connect: %v", err) + } + defer drv.Close() + + result, err := drv.EnrollRecovery(wantEnroll, wantSig) + if err != nil { + t.Fatalf("EnrollRecovery: %v", err) + } + if ok, _ := result["ok"].(bool); !ok { + t.Errorf("result = %+v, want ok=true", result) + } +}