-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
timestamp validation & improved req validation (#35)
- Loading branch information
Showing
8 changed files
with
219 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 0 additions & 47 deletions
47
listener/internal/api/validation/DecodeAndValidateRequests.go
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
33 changes: 0 additions & 33 deletions
33
listener/internal/api/validation/IsValidPayloadFormat_test.go
This file was deleted.
Oops, something went wrong.
109 changes: 109 additions & 0 deletions
109
listener/internal/api/validation/ValidateAndDecodeRequests.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
102
listener/internal/api/validation/ValidateAndDecodeRequests_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters