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.
This commit is contained in:
you
2026-04-10 00:44:48 +00:00
parent a7ada12f6d
commit 68c669f4a4
8 changed files with 141 additions and 94 deletions
+4 -28
View File
@@ -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 {
+12 -38
View File
@@ -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")
}
}
+3
View File
@@ -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
+4 -28
View File
@@ -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 {
+89
View File
@@ -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")
}
}
+3
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
module github.com/meshcore-analyzer/sigvalidate
go 1.22
+23
View File
@@ -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)
}