mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 20:18:23 +00:00
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:
+4
-28
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/meshcore-analyzer/sigvalidate
|
||||
|
||||
go 1.22
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user