trustedagents

package module
v0.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

README

trustedagents

ci codecov

Trusted-agents plugin for the Pilot Protocol daemon. Ships an embedded allowlist of public node IDs that the daemon auto-accepts handshake requests from, plus a 1-hour refresher loop that pulls the canonical list from this repo on a schedule.

Install

import "github.com/pilot-protocol/trustedagents"

Usage

// Lookup (node_id only):
name, ok := trustedagents.IsTrusted(nodeID)
_ = name
_ = ok

// Lookup with pubkey pin enforcement (preferred when the
// authenticated peer key is in scope, e.g. inbound handshake):
name, ok = trustedagents.IsTrustedWithKey(nodeID, peerPubKey)

// Daemon registration:
rt.Register(trustedagents.NewService())

Optional pubkey pinning

Each entry may carry an optional public_key (base64 std-encoded Ed25519) that pins the node_id to a specific key:

{ "hostname": "list-agents", "node_id": 14161, "public_key": "BASE64_ED25519_PUBKEY" }

IsTrustedWithKey(nodeID, peerPubKey) enforces the binding: if an entry has a public_key, the authenticated peer key must match it (constant-time compare) or the peer is not trusted. Entries without a public_key — every entry shipped today — fall back to node_id-only trust, so adding pins is fully backward-compatible. IsTrusted(nodeID) is unchanged and still answers the key-less question for callers with no peer key in scope.

This closes audit finding H4: without a pin, taking over any trusted node_id (or a registry mapping a trusted node_id to an attacker key) inherits full auto-approve trust. Enforcement at the inbound auto-accept path requires upstream wiring — see the TODO on Service.IsTrustedWithKey.

Layout

File What it does
data.go Embedded JSON list. Load, All, IsTrusted(nodeID) → (name, ok), IsTrustedWithKey(nodeID, pubKey) → (name, ok), SetForTest.
runtime.go Run(ctx) — periodic fetcher over HTTPS to raw.githubusercontent.com.
service.go *Servicecoreapi.Service adapter (Name/Order/Start/Stop + IsTrusted). Build tag !no_trustedagents.
service_disabled.go Stub *Service when build tag no_trustedagents is set.
trusted-agents.json The list itself. PRs adding entries land here.

Updating the list

Edit trusted-agents.json and open a PR. Once merged, daemons in the field pick up the new list on their next 1-hour refresh tick. Brand-new daemons get the embedded copy compiled into the binary.

Build tags

Tag Effect
no_trustedagents Compiles a stub that always returns ("", false) from IsTrusted. Used by integration tests that need a clean trust state.

License

AGPL-3.0-or-later. See LICENSE.

Documentation

Overview

Package trustedagents holds the build-time-embedded list of node IDs that the daemon auto-accepts handshake requests from. The data layer is utility-tier so both the daemon plugin (plugins/trustedagents) and the CLI (cmd/pilotctl) can read it without violating the strict downward layer rule.

The list is plain JSON in this directory, embedded at build time and refreshed hourly from raw.githubusercontent.com by plugins/trustedagents.Run. Authenticity comes from HTTPS to GitHub plus repo write access — there is no separate signature check.

Adding an agent: edit trusted-agents.json, commit. Daemons in the field pick it up within ~1h. Brand-new daemons get the embedded copy from the binary, so the feature works on first boot even airgapped.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EmbeddedJSON

func EmbeddedJSON() []byte

EmbeddedJSON returns the bytes of the embedded JSON list. Exposed for the plugin's HTTP refresher which needs to compare fetched bytes against the embedded baseline at startup.

func IsTrusted

func IsTrusted(nodeID uint32) (string, bool)

IsTrusted reports whether nodeID is in the trusted-agents list. The caller MUST verify the (node_id, public_key) binding at the registry before acting on a true result — this package only checks the list.

IsTrusted does NOT consult the optional per-entry pubkey pin: it answers the node_id-only question for callers that have no authenticated key in scope. Callers that DO have the authenticated peer key (e.g. the inbound handshake auto-accept path) MUST prefer IsTrustedWithKey so a present pin is enforced.

func IsTrustedWithKey added in v0.2.4

func IsTrustedWithKey(nodeID uint32, pubKey []byte) (string, bool)

IsTrustedWithKey reports whether nodeID is trusted given the authenticated peer's Ed25519 public key.

  • nodeID not in the list → ("", false)
  • entry HAS a pinned PublicKey → trusted ONLY if pubKey equals the pin (constant-time compare). A mismatch — or an empty/short pubKey when a pin is required — is ("", false).
  • entry has NO pin (every entry today) → trusted by node_id alone, preserving IsTrusted's behavior. The unpinned match is logged at debug so finding-H4 exposure is observable until pins are added.

Pass the AUTHENTICATED key (the one the peer proved possession of in the handshake), never an unverified claim — otherwise the pin adds nothing.

func Load

func Load(raw []byte) error

Load parses raw JSON and atomically replaces the active list. Safe to call from any goroutine. Used by plugins/trustedagents.fetchOnce after each successful HTTP refresh.

func Run

func Run(ctx context.Context)

Run polls the canonical URL on a timer, replacing the active list whenever a new one is fetched. Blocks until ctx is cancelled. The first fetch is delayed 0–30s so a fleet rebooting at the same time doesn't thunder the URL.

func SetForTest

func SetForTest(agents []Agent) (restore func())

SetForTest replaces the active list with agents and returns a restore function that reloads the embedded list. Test-only — never call from production code.

func VerifyAndStripSig

func VerifyAndStripSig(raw []byte) ([]byte, error)

VerifyAndStripSig checks the ed25519 signature embedded in the fetched JSON. If no "signature" field is present the raw body is returned as-is (backward-compatible with unsigned lists). If the field is present the signature is verified against embeddedPubKey; on mismatch an error is returned so the caller falls back to the embedded list.

Types

type Agent

type Agent struct {
	Hostname  string `json:"hostname"`
	Address   string `json:"address"`
	NodeID    uint32 `json:"node_id"`
	PublicKey string `json:"public_key,omitempty"`
}

Agent is one entry in the trusted-agents list. Match is by NodeID; Hostname and Address are kept for logs and `pilotctl trusted list`. Other JSON fields in the source file (tier, description, ...) are silently ignored on unmarshal — we don't care about them at runtime.

PublicKey is OPTIONAL: a base64 (std encoding) Ed25519 public key pinning the node_id to a specific key. When present, the inbound auto-accept path MUST verify the authenticated peer's key equals it (see IsTrustedWithKey). When absent — as for every entry shipped today — trust falls back to node_id alone, preserving current behavior. Pinning closes audit finding H4: without it, taking over a trusted node_id (or a registry that maps a trusted node_id to an attacker key) inherits full auto-approve trust.

func All

func All() []Agent

All returns a copy of the current list. Used by `pilotctl trusted list`.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the L11 plugin adapter. Implements both coreapi.Service (lifecycle) and coreapi.TrustChecker (trust gate). Daemon stores it twice — once in the plugin registry, once as the trust checker — but it's the same struct.

func NewService

func NewService() *Service

NewService returns a Service ready for daemon.RegisterPlugin and daemon.RegisterTrustChecker.

func (*Service) IsTrusted

func (s *Service) IsTrusted(nodeID uint32) (string, bool)

IsTrusted is the coreapi.TrustChecker side of the plugin. Delegates to the package-global allowlist that Run() maintains.

func (*Service) IsTrustedWithKey added in v0.2.4

func (s *Service) IsTrustedWithKey(nodeID uint32, pubKey []byte) (string, bool)

IsTrustedWithKey is the pubkey-pinned trust gate (audit finding H4). It enforces a per-entry Ed25519 pin when one is present and otherwise falls back to node_id-only trust. Delegates to the package-global allowlist.

TODO(H4 wiring): the inbound auto-accept call site lives in protocol/plugins/handshake (handshake.go ~L639), where it calls hm.rt.IsTrusted(peerNodeID) via the coreapi.TrustChecker interface. The authenticated peer key IS in scope there as msg.PublicKey (it is already stored into the TrustRecord). To actually ENFORCE pinning, two upstream changes are needed, in this order:

  1. common/coreapi.TrustChecker: add IsTrustedWithKey(nodeID uint32, pubKey []byte) (string, bool).
  2. protocol/plugins/handshake: replace the auto-accept hm.rt.IsTrusted(peerNodeID) call with hm.rt.IsTrustedWithKey(peerNodeID, msg.PublicKey).

Until those land, this method is callable directly but the daemon's handshake path still routes through IsTrusted, so pins are stored and validated on Load but not yet enforced at auto-accept. That is safe: every shipped entry is unpinned, so behavior is unchanged.

func (*Service) Name

func (s *Service) Name() string

func (*Service) Order

func (s *Service) Order() int

Order: 50 — must start BEFORE handshake (~order 70) so the allowlist is populated when the first trust handshake arrives.

func (*Service) Start

func (s *Service) Start(ctx context.Context, deps coreapi.Deps) error

func (*Service) Stop

func (s *Service) Stop(ctx context.Context) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL