Skip to content

Commit

Permalink
timestamp validation & improved req validation (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marketen authored May 15, 2024
1 parent 51e14c5 commit fdb87d4
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 98 deletions.
4 changes: 2 additions & 2 deletions listener/internal/api/handlers/postNewSignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func PostNewSignature(w http.ResponseWriter, r *http.Request, dbCollection *mong
return
}
// Decode and validate incoming requests
validRequests, err := validation.DecodeAndValidateRequests(requests)
validRequests, err := validation.ValidateAndDecodeRequests(requests)
if err != nil {
logger.Error("Failed to decode request body: " + err.Error())
respondError(w, http.StatusBadRequest, "Invalid request format")
Expand Down Expand Up @@ -60,7 +60,7 @@ func PostNewSignature(w http.ResponseWriter, r *http.Request, dbCollection *mong

validSignatures := []types.SignatureRequestDecoded{}
for _, req := range activeValidators {
isValidSignature, err := validation.IsValidSignature(req)
isValidSignature, err := validation.VerifySignature(req)
if err != nil {
logger.Error("Failed to validate signature: " + err.Error())
continue
Expand Down
47 changes: 0 additions & 47 deletions listener/internal/api/validation/DecodeAndValidateRequests.go

This file was deleted.

10 changes: 0 additions & 10 deletions listener/internal/api/validation/IsValidPayloadFormat.go

This file was deleted.

33 changes: 0 additions & 33 deletions listener/internal/api/validation/IsValidPayloadFormat_test.go

This file was deleted.

109 changes: 109 additions & 0 deletions listener/internal/api/validation/ValidateAndDecodeRequests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package validation

import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"strconv"
"time"

"github.com/dappnode/validator-monitoring/listener/internal/api/types"
"github.com/dappnode/validator-monitoring/listener/internal/logger"
)

// ValidateAndDecodeRequests filters out Recieved Invalid Reques from the input array. The returned array contains only the valid requests, with the payload decoded.
func ValidateAndDecodeRequests(requests []types.SignatureRequest) ([]types.SignatureRequestDecoded, error) {
var validRequests []types.SignatureRequestDecoded
for _, req := range requests {
if !isValidCodedRequest(&req) {
logger.Debug("Skipping request due to invalid fields or format.")
continue
}
decodedPayload, err := decodeAndValidatePayload(req.Payload)
if err != nil {
logger.Error("Failed to decode payload: " + err.Error())
continue
}
validRequests = append(validRequests, types.SignatureRequestDecoded{
DecodedPayload: decodedPayload,
Payload: req.Payload,
Pubkey: req.Pubkey,
Signature: req.Signature,
Network: req.Network,
Tag: req.Tag,
})
}
return validRequests, nil
}

// isValidCodedRequest checks if the request has all the required fields, the correct signature format, and a valid BLS pubkey
// TODO: we should consider having an enum for Network and Tag fields and validate them as well.
func isValidCodedRequest(req *types.SignatureRequest) bool {
// Check for any empty required fields
if req.Network == "" || req.Tag == "" || req.Signature == "" || req.Payload == "" || req.Pubkey == "" {
logger.Debug("Received Invalid Request: One or more required fields are empty.")
return false
}

// Check if the signature format is correct (should start with '0x' and be 194 characters long)
if len(req.Signature) != 194 || req.Signature[:2] != "0x" {
logger.Debug("Received Invalid Request: Signature format is incorrect.")
return false
}

// Validate BLS public key: should start with '0x' and be 98 characters long (96 hex characters + '0x')
if len(req.Pubkey) != 98 || req.Pubkey[:2] != "0x" {
logger.Debug("Received Invalid Request: Public key format is incorrect.")
return false
}

// Decode the public key to make sure it's a valid hex and exactly 48 bytes long
pubKeyBytes, err := hex.DecodeString(req.Pubkey[2:]) // Skip '0x' prefix
if err != nil || len(pubKeyBytes) != 48 {
logger.Debug("Received Invalid Request: Public key is not a valid BLS key.")
return false
}

return true
}

// decodeAndValidatePayload decodes the base64 encoded payload and validates the format. It must be a valid JSON with the correct fields:
// - Platform: "dappnode"
// - Type: "PROOF_OF_VALIDATION"
// - Timestamp: a valid Unix timestamp within the last 30 days
func decodeAndValidatePayload(payload string) (types.DecodedPayload, error) {
// Decode the base64 payload into bytes and unmarshal into DecodedPayload
decodedBytes, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return types.DecodedPayload{}, errors.New("invalid base64 encoding")
}

var decodedPayload types.DecodedPayload
if err := json.Unmarshal(decodedBytes, &decodedPayload); err != nil {
return types.DecodedPayload{}, errors.New("error unmarshalling JSON")
}

// validate platform
if decodedPayload.Platform != "dappnode" {
return types.DecodedPayload{}, errors.New("invalid payload: must be from 'dappnode'")
}

// validate type
if decodedPayload.Type != "PROOF_OF_VALIDATION" {
return types.DecodedPayload{}, errors.New("invalid type: must be 'PROOF_OF_VALIDATION'")
}

// validate timestamp. Must be a valid Unix timestamp within the last 30 days
timestampSecs, err := strconv.ParseInt(decodedPayload.Timestamp, 10, 64)
if err != nil {
return types.DecodedPayload{}, errors.New("timestamp is not a valid Unix timestamp")
}

timestampTime := time.Unix(timestampSecs, 0)
if time.Since(timestampTime) > 30*24*time.Hour || decodedPayload.Timestamp == "" {
return types.DecodedPayload{}, errors.New("invalid or old timestamp: must be within the last 30 days and not empty")
}

return decodedPayload, nil
}
102 changes: 102 additions & 0 deletions listener/internal/api/validation/ValidateAndDecodeRequests_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package validation

import (
"encoding/base64"
"strconv"
"testing"
"time"

"github.com/dappnode/validator-monitoring/listener/internal/api/types"
)

// Helper function to repeat a string
func repeatString(s string, count int) string {
var result string
for i := 0; i < count; i++ {
result += s
}
return result
}

func TestValidateAndDecodeRequests(t *testing.T) {
// Setup current time for timestamp tests
currentTime := time.Now()
validTimestamp := currentTime.AddDate(0, 0, -10).Unix() // 10 days ago, within valid range
oldTimestamp := currentTime.AddDate(0, -2, 0).Unix() // 2 months ago, too old

// Create a base64 encoded valid payload with a Unix timestamp and correct type
validEncodedPayload := base64.StdEncoding.EncodeToString([]byte(`{"type":"PROOF_OF_VALIDATION","platform":"dappnode","timestamp":"` + strconv.FormatInt(validTimestamp, 10) + `"}`))
oldEncodedPayload := base64.StdEncoding.EncodeToString([]byte(`{"type":"PROOF_OF_VALIDATION","platform":"dappnode","timestamp":"` + strconv.FormatInt(oldTimestamp, 10) + `"}`))
invalidTypePayload := base64.StdEncoding.EncodeToString([]byte(`{"type":"INVALID_TYPE","platform":"dappnode","timestamp":"` + strconv.FormatInt(validTimestamp, 10) + `"}`))

// Example of a valid BLS public key for Eth2
validBlsPubkey := "0xa06251962339450df57631d128fa54e4d54e2d17015571f1bcccd9b45c6ea971245f209cc9be087d5440bec19495a99a"
invalidBlsPubkey := "0x123456" // Example of an invalid BLS public key (too short)

// Mock requests
requests := []types.SignatureRequest{
{ // Valid request
Payload: validEncodedPayload,
Pubkey: validBlsPubkey,
Signature: "0x" + repeatString("a", 192), // valid signature
Network: "mainnet",
Tag: "tag1",
},
{ // Missing fields
Payload: "",
Pubkey: "",
Signature: "",
Network: "",
Tag: "",
},
{ // Invalid signature format
Payload: validEncodedPayload,
Pubkey: validBlsPubkey,
Signature: "bad_signature",
Network: "mainnet",
Tag: "tag2",
},
{ // Old timestamp
Payload: oldEncodedPayload,
Pubkey: validBlsPubkey,
Signature: "0x" + repeatString("a", 192),
Network: "mainnet",
Tag: "tag3",
},
{ // Invalid type
Payload: invalidTypePayload,
Pubkey: validBlsPubkey,
Signature: "0x" + repeatString("a", 192),
Network: "mainnet",
Tag: "tag4",
},
{ // Invalid BLS public key
Payload: validEncodedPayload,
Pubkey: invalidBlsPubkey,
Signature: "0x" + repeatString("a", 192),
Network: "mainnet",
Tag: "tag5",
},
}

// Expected results is a slice of structs with the expected number of valid requests and whether we expect an error.
expectedResults := []struct {
expectError bool
expectedLen int
}{
{false, 1}, // Expect one valid decoded request
{false, 0}, // No valid request, all fields missing
{false, 0}, // Signature format invalid
{false, 0}, // Old timestamp
{false, 0}, // Invalid type
{false, 0}, // Invalid BLS public key
}

// Run tests. We expect the number of valid requests to match the expected results
for i, req := range requests {
decodedRequests, _ := ValidateAndDecodeRequests([]types.SignatureRequest{req})
if len(decodedRequests) != expectedResults[i].expectedLen {
t.Errorf("Test %d failed, expected %d valid requests, got %d", i+1, expectedResults[i].expectedLen, len(decodedRequests))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/herumi/bls-eth-go-binary/bls"
)

func IsValidSignature(req types.SignatureRequestDecoded) (bool, error) {
func VerifySignature(req types.SignatureRequestDecoded) (bool, error) {
// Initialize the BLS system
if err := bls.Init(bls.BLS12_381); err != nil {
logger.Error("Failed to initialize BLS system: " + err.Error())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/herumi/bls-eth-go-binary/bls"
)

func TestIsValidSignature(t *testing.T) {
func TestVerifySignature(t *testing.T) {
// Initialize BLS
if err := bls.Init(bls.BLS12_381); err != nil {
t.Fatalf("Failed to initialize BLS: %v", err)
Expand Down Expand Up @@ -46,7 +46,7 @@ func TestIsValidSignature(t *testing.T) {
}

// Validate the signature
isValid, err := IsValidSignature(req)
isValid, err := VerifySignature(req)
if err != nil {
t.Errorf("IsValidSignature returned an error: %v", err)
}
Expand All @@ -55,8 +55,8 @@ func TestIsValidSignature(t *testing.T) {
}
}

// TestIsValidSignatureError tests the IsValidSignature function for expected errors
func TestIsValidSignatureError(t *testing.T) {
// TestVerifySignatureError tests the IsValidSignature function for expected errors
func TestVerifySignatureError(t *testing.T) {
// Initialize BLS just as in normal tests
if err := bls.Init(bls.BLS12_381); err != nil {
t.Fatalf("Failed to initialize BLS: %v", err)
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestIsValidSignatureError(t *testing.T) {
}

// Validate the signature
isValid, err := IsValidSignature(req)
isValid, err := VerifySignature(req)
if err == nil {
t.Errorf("Expected an error for invalid signature data, but got none")
}
Expand Down

0 comments on commit fdb87d4

Please sign in to comment.