From 68c669f4a4950da06cdca04870a4107b55095d07 Mon Sep 17 00:00:00 2001 From: you Date: Fri, 10 Apr 2026 00:44:48 +0000 Subject: [PATCH] Extract validateAdvertSignature to shared package, accept []byte, add server tests - Extract signature validation to internal/sigvalidate/ shared package - Change function signature to accept []byte instead of hex strings, eliminating unnecessary hex encode/decode round-trip - Add signature validation tests to cmd/server/decoder_test.go - Both cmd/server and cmd/ingestor now import from the shared package Addresses all review feedback on PR #686. --- cmd/ingestor/decoder.go | 32 ++--------- cmd/ingestor/decoder_test.go | 50 ++++------------ cmd/ingestor/go.mod | 3 + cmd/server/decoder.go | 32 ++--------- cmd/server/decoder_test.go | 89 +++++++++++++++++++++++++++++ cmd/server/go.mod | 3 + internal/sigvalidate/go.mod | 3 + internal/sigvalidate/sigvalidate.go | 23 ++++++++ 8 files changed, 141 insertions(+), 94 deletions(-) create mode 100644 internal/sigvalidate/go.mod create mode 100644 internal/sigvalidate/sigvalidate.go diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index c9dabb2e..79a11037 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -2,7 +2,6 @@ package main import ( "crypto/aes" - "crypto/ed25519" "crypto/hmac" "crypto/sha256" "encoding/binary" @@ -12,6 +11,8 @@ import ( "math" "strings" "unicode/utf8" + + "github.com/meshcore-analyzer/sigvalidate" ) // Route type constants (header bits 1-0) @@ -217,26 +218,6 @@ func decodeAck(buf []byte) Payload { } } -func validateAdvertSignature(pubKeyHex, signatureHex string, timestamp uint32, appdata []byte) (bool, error) { - pubKey, err := hex.DecodeString(pubKeyHex) - if err != nil || len(pubKey) != 32 { - return false, fmt.Errorf("invalid pubkey") - } - - signature, err := hex.DecodeString(signatureHex) - if err != nil || len(signature) != 64 { - return false, fmt.Errorf("invalid signature") - } - - // Signed data: pubKey (32) + timestamp (4 LE) + appdata - message := make([]byte, 32+4+len(appdata)) - copy(message[0:32], pubKey) - binary.LittleEndian.PutUint32(message[32:36], timestamp) - copy(message[36:], appdata) - - return ed25519.Verify(ed25519.PublicKey(pubKey), message, signature), nil -} - func decodeAdvert(buf []byte, validateSignatures bool) Payload { if len(buf) < 100 { return Payload{Type: "ADVERT", Error: "too short for advert", RawHex: hex.EncodeToString(buf)} @@ -256,13 +237,8 @@ func decodeAdvert(buf []byte, validateSignatures bool) Payload { } if validateSignatures { - valid, err := validateAdvertSignature(pubKey, signature, timestamp, appdata) - if err != nil { - f := false - p.SignatureValid = &f - } else { - p.SignatureValid = &valid - } + valid := sigvalidate.ValidateAdvertSignature(buf[0:32], buf[36:100], timestamp, appdata) + p.SignatureValid = &valid } if len(appdata) > 0 { diff --git a/cmd/ingestor/decoder_test.go b/cmd/ingestor/decoder_test.go index 6b881e57..1961bd2d 100644 --- a/cmd/ingestor/decoder_test.go +++ b/cmd/ingestor/decoder_test.go @@ -10,6 +10,8 @@ import ( "math" "strings" "testing" + + "github.com/meshcore-analyzer/sigvalidate" ) func TestDecodeHeaderRoutTypes(t *testing.T) { @@ -1610,70 +1612,42 @@ func TestValidateAdvertSignature(t *testing.T) { if err != nil { t.Fatal(err) } - pubHex := hex.EncodeToString(pub) var timestamp uint32 = 1234567890 appdata := []byte{0x02, 0x11, 0x22} // flags + some data - // Build the message the same way validateAdvertSignature does + // Build the message the same way ValidateAdvertSignature does message := make([]byte, 32+4+len(appdata)) copy(message[0:32], pub) binary.LittleEndian.PutUint32(message[32:36], timestamp) copy(message[36:], appdata) sig := ed25519.Sign(priv, message) - sigHex := hex.EncodeToString(sig) // Valid signature - valid, err := validateAdvertSignature(pubHex, sigHex, timestamp, appdata) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !valid { + if !sigvalidate.ValidateAdvertSignature(pub, sig, timestamp, appdata) { t.Error("expected valid signature") } // Tampered appdata → invalid badAppdata := []byte{0x03, 0x11, 0x22} - valid, err = validateAdvertSignature(pubHex, sigHex, timestamp, badAppdata) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if valid { + if sigvalidate.ValidateAdvertSignature(pub, sig, timestamp, badAppdata) { t.Error("expected invalid signature with tampered appdata") } // Wrong timestamp → invalid - valid, err = validateAdvertSignature(pubHex, sigHex, timestamp+1, appdata) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if valid { + if sigvalidate.ValidateAdvertSignature(pub, sig, timestamp+1, appdata) { t.Error("expected invalid signature with wrong timestamp") } - // Malformed pubkey - _, err = validateAdvertSignature("ZZZZ", sigHex, timestamp, appdata) - if err == nil { - t.Error("expected error for malformed pubkey hex") + // Wrong length pubkey → false + if sigvalidate.ValidateAdvertSignature([]byte{0xAA, 0xBB}, sig, timestamp, appdata) { + t.Error("expected false for short pubkey") } - // Wrong length pubkey - _, err = validateAdvertSignature("AABB", sigHex, timestamp, appdata) - if err == nil { - t.Error("expected error for short pubkey") - } - - // Malformed signature - _, err = validateAdvertSignature(pubHex, "ZZZZ", timestamp, appdata) - if err == nil { - t.Error("expected error for malformed signature hex") - } - - // Wrong length signature - _, err = validateAdvertSignature(pubHex, "AABB", timestamp, appdata) - if err == nil { - t.Error("expected error for short signature") + // Wrong length signature → false + if sigvalidate.ValidateAdvertSignature(pub, []byte{0xAA, 0xBB}, timestamp, appdata) { + t.Error("expected false for short signature") } } diff --git a/cmd/ingestor/go.mod b/cmd/ingestor/go.mod index bd0cfdb6..b87085c2 100644 --- a/cmd/ingestor/go.mod +++ b/cmd/ingestor/go.mod @@ -5,11 +5,14 @@ go 1.22 require ( github.com/eclipse/paho.mqtt.golang v1.5.0 github.com/meshcore-analyzer/geofilter v0.0.0 + github.com/meshcore-analyzer/sigvalidate v0.0.0-00010101000000-000000000000 modernc.org/sqlite v1.34.5 ) replace github.com/meshcore-analyzer/geofilter => ../../internal/geofilter +replace github.com/meshcore-analyzer/sigvalidate => ../../internal/sigvalidate + require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/cmd/server/decoder.go b/cmd/server/decoder.go index 87ae2327..f3dd901d 100644 --- a/cmd/server/decoder.go +++ b/cmd/server/decoder.go @@ -1,7 +1,6 @@ package main import ( - "crypto/ed25519" "crypto/sha256" "encoding/binary" "encoding/hex" @@ -10,6 +9,8 @@ import ( "math" "strings" "time" + + "github.com/meshcore-analyzer/sigvalidate" ) // Route type constants (header bits 1-0) @@ -190,26 +191,6 @@ func decodeAck(buf []byte) Payload { } } -func validateAdvertSignature(pubKeyHex, signatureHex string, timestamp uint32, appdata []byte) (bool, error) { - pubKey, err := hex.DecodeString(pubKeyHex) - if err != nil || len(pubKey) != 32 { - return false, fmt.Errorf("invalid pubkey") - } - - signature, err := hex.DecodeString(signatureHex) - if err != nil || len(signature) != 64 { - return false, fmt.Errorf("invalid signature") - } - - // Signed data: pubKey (32) + timestamp (4 LE) + appdata - message := make([]byte, 32+4+len(appdata)) - copy(message[0:32], pubKey) - binary.LittleEndian.PutUint32(message[32:36], timestamp) - copy(message[36:], appdata) - - return ed25519.Verify(ed25519.PublicKey(pubKey), message, signature), nil -} - func decodeAdvert(buf []byte, validateSignatures bool) Payload { if len(buf) < 100 { return Payload{Type: "ADVERT", Error: "too short for advert", RawHex: hex.EncodeToString(buf)} @@ -229,13 +210,8 @@ func decodeAdvert(buf []byte, validateSignatures bool) Payload { } if validateSignatures { - valid, err := validateAdvertSignature(pubKey, signature, timestamp, appdata) - if err != nil { - f := false - p.SignatureValid = &f - } else { - p.SignatureValid = &valid - } + valid := sigvalidate.ValidateAdvertSignature(buf[0:32], buf[36:100], timestamp, appdata) + p.SignatureValid = &valid } if len(appdata) > 0 { diff --git a/cmd/server/decoder_test.go b/cmd/server/decoder_test.go index 44b6c73a..f0f19cb2 100644 --- a/cmd/server/decoder_test.go +++ b/cmd/server/decoder_test.go @@ -1,7 +1,11 @@ package main import ( + "crypto/ed25519" + "encoding/binary" "testing" + + "github.com/meshcore-analyzer/sigvalidate" ) func TestDecodeHeader_TransportFlood(t *testing.T) { @@ -403,3 +407,88 @@ func TestDecodePacket_TraceFullyCompleted(t *testing.T) { t.Errorf("expected 3 hops, got %d", len(pkt.Path.Hops)) } } + +func TestValidateAdvertSignature(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + var timestamp uint32 = 1234567890 + appdata := []byte{0x02, 0x11, 0x22} + + message := make([]byte, 32+4+len(appdata)) + copy(message[0:32], pub) + binary.LittleEndian.PutUint32(message[32:36], timestamp) + copy(message[36:], appdata) + + sig := ed25519.Sign(priv, message) + + // Valid signature + if !sigvalidate.ValidateAdvertSignature(pub, sig, timestamp, appdata) { + t.Error("expected valid signature") + } + + // Tampered appdata + if sigvalidate.ValidateAdvertSignature(pub, sig, timestamp, []byte{0x03, 0x11, 0x22}) { + t.Error("expected invalid with tampered appdata") + } + + // Wrong timestamp + if sigvalidate.ValidateAdvertSignature(pub, sig, timestamp+1, appdata) { + t.Error("expected invalid with wrong timestamp") + } + + // Short pubkey + if sigvalidate.ValidateAdvertSignature([]byte{0xAA}, sig, timestamp, appdata) { + t.Error("expected false for short pubkey") + } + + // Short signature + if sigvalidate.ValidateAdvertSignature(pub, []byte{0xBB}, timestamp, appdata) { + t.Error("expected false for short signature") + } +} + +func TestDecodeAdvertWithSignatureValidation(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + var timestamp uint32 = 1000000 + appdata := []byte{0x02} // repeater type + + message := make([]byte, 32+4+len(appdata)) + copy(message[0:32], pub) + binary.LittleEndian.PutUint32(message[32:36], timestamp) + copy(message[36:], appdata) + sig := ed25519.Sign(priv, message) + + // Build advert buffer: pubkey(32) + timestamp(4) + signature(64) + appdata + buf := make([]byte, 0, 101) + buf = append(buf, pub...) + ts := make([]byte, 4) + binary.LittleEndian.PutUint32(ts, timestamp) + buf = append(buf, ts...) + buf = append(buf, sig...) + buf = append(buf, appdata...) + + // With validation + p := decodeAdvert(buf, true) + if p.Error != "" { + t.Fatalf("decode error: %s", p.Error) + } + if p.SignatureValid == nil { + t.Fatal("SignatureValid should be set when validation enabled") + } + if !*p.SignatureValid { + t.Error("expected valid signature") + } + + // Without validation + p2 := decodeAdvert(buf, false) + if p2.SignatureValid != nil { + t.Error("SignatureValid should be nil when validation disabled") + } +} diff --git a/cmd/server/go.mod b/cmd/server/go.mod index 700e9d45..04f4a8a1 100644 --- a/cmd/server/go.mod +++ b/cmd/server/go.mod @@ -6,11 +6,14 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 github.com/meshcore-analyzer/geofilter v0.0.0 + github.com/meshcore-analyzer/sigvalidate v0.0.0-00010101000000-000000000000 modernc.org/sqlite v1.34.5 ) replace github.com/meshcore-analyzer/geofilter => ../../internal/geofilter +replace github.com/meshcore-analyzer/sigvalidate => ../../internal/sigvalidate + require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/internal/sigvalidate/go.mod b/internal/sigvalidate/go.mod new file mode 100644 index 00000000..39a4952a --- /dev/null +++ b/internal/sigvalidate/go.mod @@ -0,0 +1,3 @@ +module github.com/meshcore-analyzer/sigvalidate + +go 1.22 diff --git a/internal/sigvalidate/sigvalidate.go b/internal/sigvalidate/sigvalidate.go new file mode 100644 index 00000000..19d3ea61 --- /dev/null +++ b/internal/sigvalidate/sigvalidate.go @@ -0,0 +1,23 @@ +// Package sigvalidate provides Ed25519 signature validation for MeshCore adverts. +package sigvalidate + +import ( + "crypto/ed25519" + "encoding/binary" +) + +// ValidateAdvertSignature verifies an Ed25519 signature over a MeshCore advert. +// The signed message is: pubKey (32 bytes) || timestamp (4 bytes LE) || appdata. +// Returns false if pubKey is not 32 bytes or signature is not 64 bytes. +func ValidateAdvertSignature(pubKey, signature []byte, timestamp uint32, appdata []byte) bool { + if len(pubKey) != 32 || len(signature) != 64 { + return false + } + + message := make([]byte, 32+4+len(appdata)) + copy(message[0:32], pubKey) + binary.LittleEndian.PutUint32(message[32:36], timestamp) + copy(message[36:], appdata) + + return ed25519.Verify(ed25519.PublicKey(pubKey), message, signature) +}