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
5 changes: 5 additions & 0 deletions cmd/rendezvous/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func main() {
tlsKey := flag.String("tls-key", "", "TLS key file")
enableTLS := flag.Bool("tls", false, "enable TLS for registry connections")
strictDirectoryAuth := flag.Bool("strict-directory-auth", false, "WS2: require trust/shared-network authorization on directory RPCs (lookup/resolve/punch/list_*/check_trust). Default false (not enforcing, wire-compatible with old agents). Env: RENDEZVOUS_STRICT_DIRECTORY_AUTH=1.")
strictRegistrationAuth := flag.Bool("strict-registration-auth", false, "require a valid proof-of-possession signature on registrations that submit a public_key. Default false (signatures verified when present, but unsigned registrations from old agents are still accepted). Env: RENDEZVOUS_STRICT_REGISTRATION_AUTH=1.")
standbyPrimary := flag.String("standby", "", "run as hot standby replicating from the given primary address (e.g. primary:9000)")
httpAddr := flag.String("http", "", "HTTP dashboard listen address (e.g. :3000)")
logLevel := flag.String("log-level", "info", "log level (debug, info, warn, error)")
Expand Down Expand Up @@ -128,6 +129,10 @@ func main() {
r.SetStrictDirectoryAuth(true)
slog.Info("strict directory authorization enabled (WS2)")
}
if *strictRegistrationAuth || os.Getenv("RENDEZVOUS_STRICT_REGISTRATION_AUTH") == "1" {
r.SetStrictRegistrationAuth(true)
slog.Info("strict registration authorization enabled")
}
// Plumb the breaker manager into the in-process beacon so
// beacon.punch / beacon.relay / beacon.discover can be flipped from
// the same breakers.json file as the registry-side switches.
Expand Down
20 changes: 20 additions & 0 deletions directory/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package directory

import (
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
Expand Down Expand Up @@ -236,6 +237,7 @@ type Callbacks struct {
// a reaped node reclaims its old identity. Caller holds mu.Lock.
ScanNetworkMemberships func(nodeID uint32) []uint16
StrictDirectoryAuth func() bool
StrictRegistrationAuth func() bool
}

// --------------------------------------------------------------------------
Expand Down Expand Up @@ -720,6 +722,24 @@ func (st *Store) HandleRegister(
return nil, fmt.Errorf("registration requires public_key")
}

sigB64, _ := msg["signature"].(string)
strictReg := st.cb.StrictRegistrationAuth != nil && st.cb.StrictRegistrationAuth()
if sigB64 != "" {
pubKey, err := crypto.DecodePublicKey(pubKeyB64)
if err != nil {
return nil, fmt.Errorf("registration: invalid public_key")
}
sig, err := base64.StdEncoding.DecodeString(sigB64)
if err != nil {
return nil, fmt.Errorf("registration: invalid signature encoding")
}
if !crypto.Verify(pubKey, []byte(fmt.Sprintf("register:%s:%s", clientAddr, pubKeyB64)), sig) {
return nil, fmt.Errorf("registration: signature verification failed")
}
} else if strictReg {
return nil, fmt.Errorf("registration: signature required")
}

if hostname != "" {
if err := ValidateHostname(hostname); err != nil {
resp, regErr := st.HandleReRegister(pubKeyB64, listenAddr, owner, "", lanAddrs, clientVersion, relayOnly)
Expand Down
80 changes: 80 additions & 0 deletions directory/zz_ppa003_registration_signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package directory

import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"testing"

"github.com/pilot-protocol/common/crypto"
)

func TestHandleRegisterSignatureVerification(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
pubB64 := crypto.EncodePublicKey(pub)
const listenAddr = "10.0.0.1:4000"

sign := func(p ed25519.PrivateKey) string {
return base64.StdEncoding.EncodeToString(
ed25519.Sign(p, []byte(fmt.Sprintf("register:%s:%s", listenAddr, pubB64))))
}
goodSig := sign(priv)

_, otherPriv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
wrongKeySig := sign(otherPriv)

regMsg := func(sig string) map[string]interface{} {
m := map[string]interface{}{
"public_key": pubB64,
"listen_addr": listenAddr,
"owner": "alice",
}
if sig != "" {
m["signature"] = sig
}
return m
}

cases := []struct {
name string
strict bool
sig string
wantErr bool
}{
{"unsigned accepted by default (backward compatible)", false, "", false},
{"unsigned rejected when strict", true, "", true},
{"valid signature accepted (lenient)", false, goodSig, false},
{"valid signature accepted (strict)", true, goodSig, false},
{"wrong-key signature rejected (lenient verify-if-present)", false, wrongKeySig, true},
{"wrong-key signature rejected (strict)", true, wrongKeySig, true},
{"malformed signature rejected", false, "not-base64!!", true},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
st := newTestStore(t)
st.cb.StrictRegistrationAuth = func() bool { return tc.strict }
resp, err := st.HandleRegister(regMsg(tc.sig), listenAddr, nil, nil)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got resp=%v", resp)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp["type"] != "register_ok" {
t.Fatalf("expected register_ok, got %v", resp)
}
})
}
}
17 changes: 9 additions & 8 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ type Server struct {
// its own internal locking. Surfaced read-only via /api/health and
// included in the rich /api/stats payload. Reads are non-blocking
// (atomic loads) so a probe doesn't contend with a save in flight.
snapshotsTotal atomic.Int64 // successful flushSave calls
snapshotsFailed atomic.Int64 // flushSave returning an error
lastSnapshotUnixMs atomic.Int64 // wall time of last successful save
lastSnapshotDurMs atomic.Int64 // duration of last successful save
lastSnapshotSizeB atomic.Int64 // bytes written by last successful save
lastSnapshotRLockMs atomic.Int64 // s.mu.RLock hold time in phase 1
maxSnapshotDurMs atomic.Int64 // worst-ever save duration
snapshotsTotal atomic.Int64 // successful flushSave calls
snapshotsFailed atomic.Int64 // flushSave returning an error
lastSnapshotUnixMs atomic.Int64 // wall time of last successful save
lastSnapshotDurMs atomic.Int64 // duration of last successful save
lastSnapshotSizeB atomic.Int64 // bytes written by last successful save
lastSnapshotRLockMs atomic.Int64 // s.mu.RLock hold time in phase 1
maxSnapshotDurMs atomic.Int64 // worst-ever save duration

// buildInfo carries the build-time identity surfaced on
// /api/public-stats for code-verification (version, git commit, ISO
Expand Down Expand Up @@ -284,7 +284,8 @@ type Server struct {
listNodesPerNetMu sync.Mutex // guards the map itself
listNodesPerNet map[uint16]*listNodesCacheState

strictDirectoryAuth atomic.Bool
strictDirectoryAuth atomic.Bool
strictRegistrationAuth atomic.Bool
}

// listNodesCacheState is defined in the directory sub-package (R4.2).
Expand Down
19 changes: 14 additions & 5 deletions server_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ func (s *Server) StrictDirectoryAuth() bool {
return s.strictDirectoryAuth.Load()
}

func (s *Server) SetStrictRegistrationAuth(enabled bool) {
s.strictRegistrationAuth.Store(enabled)
}

func (s *Server) StrictRegistrationAuth() bool {
return s.strictRegistrationAuth.Load()
}

// SetDashboardToken gates per-network stats on the dashboard.
// Empty string restricts the dashboard to global aggregates only.
func (s *Server) SetDashboardToken(token string) {
Expand Down Expand Up @@ -941,7 +949,8 @@ func NewWithStore(beaconAddr, storePath string) *Server {
}
return nets
},
StrictDirectoryAuth: s.StrictDirectoryAuth,
StrictDirectoryAuth: s.StrictDirectoryAuth,
StrictRegistrationAuth: s.StrictRegistrationAuth,
},
)

Expand Down Expand Up @@ -1105,10 +1114,10 @@ func NewWithStore(beaconAddr, storePath string) *Server {
allow, _ := s.breakers.Allow(name)
return allow, s.breakers.Reason(name)
},
BreakerList: s.BreakerList,
BreakerSet: s.BreakerSet,
BreakerDelete: s.BreakerDelete,
HealthSnapshot: s.HealthSnapshot,
BreakerList: s.BreakerList,
BreakerSet: s.BreakerSet,
BreakerDelete: s.BreakerDelete,
HealthSnapshot: s.HealthSnapshot,
VerifyRequest: func(canonical, sigB64 string) interface{} {
return s.VerifyRequest(canonical, sigB64)
},
Expand Down
Loading