Skip to content
Closed
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
34 changes: 11 additions & 23 deletions accept/accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,39 +504,27 @@ func readMessage(r io.Reader) (map[string]interface{}, error) {
}

func writeMessage(w io.Writer, msg map[string]interface{}) error {
var body []byte
if raw, ok := msg[rawResponseKey].([]byte); ok && raw != nil {
var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(raw)))
if c, ok := w.(net.Conn); ok {
_ = c.SetWriteDeadline(time.Now().Add(writeMessageDeadline))
defer c.SetWriteDeadline(time.Time{})
}
if _, err := w.Write(lenBuf[:]); err != nil {
return err
}
if _, err := w.Write(raw); err != nil {
return err
body = raw
} else {
var err error
body, err = json.Marshal(msg)
if err != nil {
return fmt.Errorf("json encode: %w", err)
}
return nil
}

body, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("json encode: %w", err)
}

var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(body)))
frame := make([]byte, 4+len(body))
binary.BigEndian.PutUint32(frame[:4], uint32(len(body)))
copy(frame[4:], body)

if c, ok := w.(net.Conn); ok {
_ = c.SetWriteDeadline(time.Now().Add(writeMessageDeadline))
defer c.SetWriteDeadline(time.Time{})
}

if _, err := w.Write(lenBuf[:]); err != nil {
return err
}
if _, err := w.Write(body); err != nil {
if _, err := w.Write(frame); err != nil {
return err
}
return nil
Expand Down
42 changes: 41 additions & 1 deletion authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
package authz

import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"sync"
"sync/atomic"

"github.com/pilot-protocol/common/protocol"
Expand Down Expand Up @@ -197,6 +199,44 @@ func (c *Checker) IsEnterpriseNode(nodeID uint32, nodes NodeReader, nr NetworkRe
// pre-copied value of the global admin token (copy it before releasing
// the server mutex). msg must contain a "signature" field (base64).
// challenge is the pre-image string that was signed.
const (
sigCacheShardCount = 64
sigCacheShardCap = 8192
)

type sigCacheShard struct {
mu sync.Mutex
m map[[32]byte]struct{}
}

var sigCacheShards [sigCacheShardCount]sigCacheShard

func VerifyCached(pubKey, message, sig []byte) bool {
h := sha256.New()
h.Write(pubKey)
h.Write(message)
h.Write(sig)
var key [32]byte
h.Sum(key[:0])
sh := &sigCacheShards[key[0]%sigCacheShardCount]
sh.mu.Lock()
_, hit := sh.m[key]
sh.mu.Unlock()
if hit {
return true
}
if !crypto.Verify(pubKey, message, sig) {
return false
}
sh.mu.Lock()
if sh.m == nil || len(sh.m) >= sigCacheShardCap {
sh.m = make(map[[32]byte]struct{}, sigCacheShardCap)
}
sh.m[key] = struct{}{}
sh.mu.Unlock()
return true
}

func VerifyNodeSignature(pubKey []byte, adminToken string, msg map[string]interface{}, challenge string) error {
if pubKey == nil {
// No key on file — fall back to admin token auth.
Expand All @@ -213,7 +253,7 @@ func VerifyNodeSignature(pubKey []byte, adminToken string, msg map[string]interf
if err != nil {
return fmt.Errorf("invalid signature encoding: %w", err)
}
ok := crypto.Verify(pubKey, []byte(challenge), sig)
ok := VerifyCached(pubKey, []byte(challenge), sig)
if fn := loadSigVerifyHook(); fn != nil {
fn(ok)
}
Expand Down
73 changes: 73 additions & 0 deletions authz/zz_verify_cached_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package authz

import (
"crypto/ed25519"
"testing"
)

func TestVerifyCached(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
msg := []byte("heartbeat:42")
sig := ed25519.Sign(priv, msg)

if !VerifyCached(pub, msg, sig) {
t.Fatal("first verify of valid signature failed")
}
if !VerifyCached(pub, msg, sig) {
t.Fatal("cached verify of valid signature failed")
}

bad := make([]byte, len(sig))
copy(bad, sig)
bad[0] ^= 0xFF
if VerifyCached(pub, msg, bad) {
t.Fatal("tampered signature verified")
}
if VerifyCached(pub, msg, bad) {
t.Fatal("tampered signature verified on repeat (failure must not be cached)")
}

pub2, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
if VerifyCached(pub2, msg, sig) {
t.Fatal("signature verified under wrong public key despite cached success for right key")
}

msg2 := []byte("heartbeat:43")
if VerifyCached(pub, msg2, sig) {
t.Fatal("signature verified for different message despite cached success")
}
}

func TestVerifyCachedShardReset(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
msg := []byte("resolve:1:2")
sig := ed25519.Sign(priv, msg)
if !VerifyCached(pub, msg, sig) {
t.Fatal("valid signature failed")
}
for i := range sigCacheShards {
sh := &sigCacheShards[i]
sh.mu.Lock()
for k := range sh.m {
sh.m[k] = struct{}{}
}
if sh.m != nil && len(sh.m) > sigCacheShardCap {
t.Fatalf("shard %d exceeded cap: %d", i, len(sh.m))
}
sh.mu.Unlock()
}
if !VerifyCached(pub, msg, sig) {
t.Fatal("re-verify after shard inspection failed")
}
}
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.")
requireRegisterSignature := flag.Bool("require-register-signature", false, "PPA-003: require a valid proof-of-possession signature on registration. Default false (present signatures are still verified; unsigned registrations accepted for wire-compatibility). Enable only after the signing client has rolled out. Env: RENDEZVOUS_REQUIRE_REGISTER_SIGNATURE=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 *requireRegisterSignature || os.Getenv("RENDEZVOUS_REQUIRE_REGISTER_SIGNATURE") == "1" {
r.SetRequireRegisterSignature(true)
slog.Info("registration signature required (PPA-003)")
}
// 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
Loading