Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion driver/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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...)}:
Expand Down
81 changes: 81 additions & 0 deletions driver/zz_driver_simple_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package driver

import (
"encoding/binary"
"encoding/json"
"testing"
)

Expand Down Expand Up @@ -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)
}
}
Loading