diff --git a/.gitignore b/.gitignore index 58a92d6..87b4e19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .env ./listener/tmp/* ./listener/bin/* -private.pem -public.pem \ No newline at end of file + diff --git a/ci.env b/ci.env index 70cb140..680d4b5 100644 --- a/ci.env +++ b/ci.env @@ -8,4 +8,4 @@ BEACON_NODE_URL_MAINNET=http://172.33.0.27:3500 BEACON_NODE_URL_HOLESKY=http://172.33.0.27:3500 BEACON_NODE_URL_GNOSIS=http://172.33.0.27:3500 BEACON_NODE_URL_LUKSO=http://172.33.0.27:3500 -JWT_USERS_FILE=users.json.example \ No newline at end of file +JWT_USERS_FILE=users-ci.json \ No newline at end of file diff --git a/jwt/users-ci.json b/jwt/users-ci.json new file mode 100644 index 0000000..50b577a --- /dev/null +++ b/jwt/users-ci.json @@ -0,0 +1,6 @@ +{ + "stader": { + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGqOvmSNI7qMrNa1CjRK\nxmp+M9vR6AJduw//e69LwFtt2Zv0nrn/06H+NdjEuuKJAn7XaCjk1n59SxZCpUAX\n1eyy7oV/IVrdmt7yupOasXLcKbSoqov9sxvLkgNrmdZxEpQKTpMlu55srX72Ioyo\nHYONkw6PunxRPhYKCTc4YKwgCYCfOKxBk/lilWuYmH4nlQ533TOiMfiJmw+WTPCA\n9sqm9ID84FDKk0/xeukQznRoFB5ACpk2J9c7r7IoRnL5gaRGjLQ76kFzQ08ZqcHX\n9Fbkop88ktV95YqHvsL3NwkdQfrR6BtJTaQQ9lPfyYo5O37MOCE/N+RoldToL2xa\nZwIDAQAB\n-----END PUBLIC KEY-----", + "tags": ["solo"] + } +} diff --git a/listener/cmd/jwt-generator/token.jwt b/listener/cmd/jwt-generator/token.jwt new file mode 100644 index 0000000..fa55b0a --- /dev/null +++ b/listener/cmd/jwt-generator/token.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6InlvIiwidHlwIjoiSldUIn0.e30.R1e1YWwi5nacrd0fZ3Uoj6o-sTGsrXO8oojWH6HOvD9gJXNj7qNBIR16o6vR2DQAJKPNxh5nhuyFZg8kiwvjG2WCpEBPxqvklbXlGWZqtbD57fOIu7CCAyhBvIh3TkmmAiZcGFkXwjKW62FE0_JKLtBWVBFR2hROJ27oHgW3AGtK13eZvtV8P9qpB4uOYx7GZCZxzPy7zIV-SZ3GNF7kHuosfJxruMMwsk4wZiZsfVGjIPQ4EVJFX8KDw0FzmdKXwsaSsehDqopGmEgL1UDpIDsscg3sX51GvzaUfZkK0hX7iw_-q4Zvux4nLhBo8QkO8Gc2enfVWelv_2oKkLBlvA \ No newline at end of file diff --git a/listener/test/data/private.pem b/listener/test/data/private.pem new file mode 100644 index 0000000..7de75a9 --- /dev/null +++ b/listener/test/data/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuGqOvmSNI7qMrNa1CjRKxmp+M9vR6AJduw//e69LwFtt2Zv0 +nrn/06H+NdjEuuKJAn7XaCjk1n59SxZCpUAX1eyy7oV/IVrdmt7yupOasXLcKbSo +qov9sxvLkgNrmdZxEpQKTpMlu55srX72IoyoHYONkw6PunxRPhYKCTc4YKwgCYCf +OKxBk/lilWuYmH4nlQ533TOiMfiJmw+WTPCA9sqm9ID84FDKk0/xeukQznRoFB5A +Cpk2J9c7r7IoRnL5gaRGjLQ76kFzQ08ZqcHX9Fbkop88ktV95YqHvsL3NwkdQfrR +6BtJTaQQ9lPfyYo5O37MOCE/N+RoldToL2xaZwIDAQABAoIBAQCA98Qr0hYCTovi +s6SYJknEyOTJT4lUP5NdJqr8zHKla0s/S5mfB2SplDg7YAT0zUgTvrDV3wNqHbtx +r3ecKV6B9K6NsD5gDTO9Av2tDVy5jCH76KOr4YrinyWWT14Ij7czzuQGX1GcDAbk +rk1jjApl2YJAvYY2XIpUaQVLvJpov8cVrmAuBwUNzbsd35w+d0rxpaz94nmycQZt +BHPF8BvPpBa6TaS/maJWjZlvRGHdD/9F5GpJD01tSfwstw3lDG6jlKDevjeEpsnB +Of/B8lRSJd5lrHLDX/wFqY1YxxqvEkd1nZUNYlSr6L35MsLWaswFZa1gEep6tkEv +jvdsq9pBAoGBANzcoXZ4/W8Z4qi80auaaRf0uZiIniqbDey0iyYkpsmZ06EOeTMR +o0xynYmGOvF9pn/heDY+1oM+/gMGI+rDmB83I2mbp9hfv7nIq22NOerXJhSnJXve +sFn7VAmOnNrGGQLQ3oMxSC3nJcFzLsEkX3yJkL3sAMTd3DUY7y1AdLLLAoGBANXB +jXyv6eDiGYk1cPuGnqNZdS/sEZqvNbkya8SCWeHnlAKlQe2/2fGA0IYXR0vE4qlL +EhXf/nN52ruB0AvuwYpZf894/C2LrZHBCrXUqno0PGH+L3vjzv248F2OGK+wLx4E +aJ1MY3j6/vKw2oScEH5QVBnLQmybiTq9MrpgfldVAoGAa7n5z22IQA6iLaebpVX1 +eMfXVv0cGK/0hMYzMPGjoKW1QdNrbmtl+T3WdWPRkES3V5zEI8FWpEGvaA4wqquo +oWEllrdjkPhy7c1hQFgoLdGvM1erwtWFsv4RqW+0Nkl1nZf/UIJTMICUO91Qqshx +Aq+et+RLI7sLU6LL1oif4y0CgYB3HgIjjsBNYpIKZS6N7CnxK4PjbbEtux+8EX3+ +pwlBm1Re2QrRW40vSLJrVwOTFKee4ce1Svq4DRq4TRHL5IQT/eX8jxYwp1rVE3dN +drJ9oShZD1YUuxF5UJsZ93qIRS0slBZcOdpg67YxNh6/sVx7l3YWXa+paE9j3VUs +iWM+dQKBgQCMhqECHjlcRTm7AyHYSrZ0T9qnOGrqBImFYb8niTVVciDQKpMtRQxs +7uCus+9dV6y8NerIsUfiWrUhLmYXeOPOKoV4Fvykg7IdUv5515LN2D7FJukYQL2a +OAtuwY2bjDvXbjf88a2MsAeg1iwaueb8GpjU5XTKqZMN2JJy5Z9wlw== +-----END RSA PRIVATE KEY----- diff --git a/listener/test/data/public.pem b/listener/test/data/public.pem new file mode 100644 index 0000000..1be6e46 --- /dev/null +++ b/listener/test/data/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGqOvmSNI7qMrNa1CjRK +xmp+M9vR6AJduw//e69LwFtt2Zv0nrn/06H+NdjEuuKJAn7XaCjk1n59SxZCpUAX +1eyy7oV/IVrdmt7yupOasXLcKbSoqov9sxvLkgNrmdZxEpQKTpMlu55srX72Ioyo +HYONkw6PunxRPhYKCTc4YKwgCYCfOKxBk/lilWuYmH4nlQ533TOiMfiJmw+WTPCA +9sqm9ID84FDKk0/xeukQznRoFB5ACpk2J9c7r7IoRnL5gaRGjLQ76kFzQ08ZqcHX +9Fbkop88ktV95YqHvsL3NwkdQfrR6BtJTaQQ9lPfyYo5O37MOCE/N+RoldToL2xa +ZwIDAQAB +-----END PUBLIC KEY----- diff --git a/listener/test/data/token.jwt b/listener/test/data/token.jwt new file mode 100644 index 0000000..b2ef993 --- /dev/null +++ b/listener/test/data/token.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6InN0YWRlciIsInR5cCI6IkpXVCJ9.e30.AbouFU5vgAAdBaFMTclV3ddiZTf2U9xKi-qsX4PESAfGwI4e_mTrzP9F43j45vemU8yBMFOL6pwouMK70lbumfb0k1Htlx_xy2HSHNZsxLDvCR1NR0p_s8wCsIe4xuSpBtGdcgabqNFLxvaUrg0h0k42JewhguKlbCnPVz6LQpE-V9YTxZMHpxn2hevRAq7mQVXPHsXH-08QqUB1AwVwnWzeoBxMSCcs9VsiNrbxeZxqmZDHBaF5FJjnbZm1nZLb_OWm73tpDvWCuOx0ie0tW_jUbW3-tlsOSPU2tcRRNmvWuuuRMkrduTu9NGeJOmTauOkUKJ6ZoiVI76-qu1Lkqw \ No newline at end of file diff --git a/listener/test/sendSignatures_test.go b/listener/test/sendSignatures_test.go index 801b5df..3b054b0 100644 --- a/listener/test/sendSignatures_test.go +++ b/listener/test/sendSignatures_test.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "net/http" + "os" "strconv" "testing" "time" @@ -13,118 +14,211 @@ import ( "github.com/herumi/bls-eth-go-binary/bls" ) -// TestPostSignaturesIntegration tests the POST /signatures endpoint. It expects a "listener" service to be running at -// http://localhost:8080, with the proper mongoDB connected to it. The test sends a series of requests with different payloads, -// public keys, signatures, and tags to the endpoint and checks the response status code. -func TestPostSignaturesIntegration(t *testing.T) { - // Initialize BLS for the test - if err := bls.Init(bls.BLS12_381); err != nil { - t.Fatalf("Failed to initialize BLS: %v", err) +func readJWT(filePath string) (string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return "", err } + return string(data), nil +} - // Create a new HTTPExpect instance - e := httpexpect.Default(t, "http://localhost:8080") +// generateSignature generates a BLS signature and base64 encoded payload using a secret key. +func generateSignature(payload types.DecodedPayload, secretKey *bls.SecretKey) (signatureHex, payloadBase64 string) { + payloadBytes, _ := json.Marshal(payload) + signature := secretKey.SignByte(payloadBytes) + signatureHex = "0x" + signature.SerializeToHexStr() + payloadBase64 = base64.StdEncoding.EncodeToString(payloadBytes) + return +} - // Generate valid BLS keys and signature +func setupSecretKey() *bls.SecretKey { var secretKey bls.SecretKey secretKey.SetByCSPRNG() - publicKey := secretKey.GetPublicKey() - publicKeyHex := "0x" + publicKey.SerializeToHexStr() - - // Prepare timestamps and payloads - currentTime := time.Now() - validTimestamp := currentTime.AddDate(0, 0, -10).UnixMilli() // timestamp is 10 days ago + return &secretKey +} - validDecodedPayload := types.DecodedPayload{ - Type: "PROOF_OF_VALIDATION", - Platform: "dappnode", - Timestamp: strconv.FormatInt(validTimestamp, 10), +func TestPostSignaturesIntegration(t *testing.T) { + // BLS initialization + if err := bls.Init(bls.BLS12_381); err != nil { + t.Fatalf("Failed to initialize BLS: %v", err) } - payloadBytes, _ := json.Marshal(validDecodedPayload) - validPayload := base64.StdEncoding.EncodeToString(payloadBytes) - signature := secretKey.SignByte(payloadBytes) - validSignature := "0x" + signature.SerializeToHexStr() - invalidDecodedPayload := types.DecodedPayload{ - Type: "INVALID_TYPE", - Platform: "dappnode", - Timestamp: strconv.FormatInt(validTimestamp, 10), + e := httpexpect.Default(t, "http://localhost:8080") + + // Setup test data + currentTime := time.Now().UnixMilli() + secretKey := setupSecretKey() + secretKey2 := setupSecretKey() + + // Read JWT token from file + jwtToken, err := readJWT("data/token.jwt") + if err != nil { + t.Fatalf("Failed to read JWT: %v", err) } - invalidPayloadBytes, _ := json.Marshal(invalidDecodedPayload) - invalidPayload := base64.StdEncoding.EncodeToString(invalidPayloadBytes) - // Define test cases - // TODO: we should add the expected message for each case too, besides the expected code - tests := []struct { + testCases := []struct { description string - payload string - pubkey string - signature string - tag types.Tag + payload types.DecodedPayload + secretKey *bls.SecretKey expectedCode int + tag types.Tag + invalidKey bool // Use invalid key if true + customSig string // Use custom signature if not empty }{ { - description: "Valid request", - payload: validPayload, - pubkey: publicKeyHex, - signature: validSignature, - tag: types.Solo, + description: "Valid request solo tag", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), // 10 days ago + }, + secretKey: secretKey, expectedCode: http.StatusOK, + tag: types.Solo, }, { - description: "Invalid payload format", - payload: invalidPayload, - pubkey: publicKeyHex, - signature: validSignature, + description: "Valid request solo tag with new timestamp", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-5*24*60*60*1000, 10), // 5 days ago + }, + secretKey: secretKey, // Reuse the key for a different payload + expectedCode: http.StatusOK, tag: types.Solo, - expectedCode: http.StatusBadRequest, }, { - description: "Valid signature format arbitrary bytes signed, shouldnt pass the crypto verification", - payload: validPayload, - pubkey: publicKeyHex, - signature: "0x8bc341f083e34d27b8df9f48b0bfcdaa7ed009146969cee0d0d4e03afd383242e1767627d5e2ef50cce410dd02ed88280bb91309f96e5ad1ad31b204f1ed5e64a43cdf3c32603450b477a40df366f3ae145014cade0f22d588786f4f07bc7c7d", + description: "Valid request solo tag with new timestamp and pubkey", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-5*24*60*60*1000, 10), // 5 days ago + }, + secretKey: secretKey2, // Reuse the key for a different payload + expectedCode: http.StatusOK, tag: types.Solo, - expectedCode: http.StatusBadRequest, }, { - description: "Invalid BLS public key", - payload: validPayload, - pubkey: "0xinvalidKey", - signature: validSignature, + description: "Valid request ssv tag", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), + }, + secretKey: secretKey, + expectedCode: http.StatusOK, + tag: types.Ssv, + }, + { + description: "Invalid payload format", + payload: types.DecodedPayload{ + Type: "INVALID_TYPE", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), + }, + secretKey: secretKey, + expectedCode: http.StatusBadRequest, tag: types.Solo, + }, + { + description: "Valid signature format arbitrary bytes signed, shouldn't pass the crypto verification", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), + }, + secretKey: secretKey, expectedCode: http.StatusBadRequest, + tag: types.Solo, + customSig: "0x8bc341f083e34d27b8df9f48b0bfcdaa7ed009146969cee0d0d4e03afd383242e1767627d5e2ef50cce410dd02ed88280bb91309f96e5ad1ad31b204f1ed5e64a43cdf3c32603450b477a40df366f3ae145014cade0f22d588786f4f07bc7c7d", }, { - description: "Invalid JSON format", - payload: `{bad json}`, - pubkey: publicKeyHex, - signature: validSignature, + description: "Invalid BLS public key", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), + }, + secretKey: secretKey, + expectedCode: http.StatusBadRequest, tag: types.Solo, + invalidKey: true, // Use an invalid key in test + }, + { + description: "Invalid JSON format", + payload: types.DecodedPayload{ + Type: "INVALID_JSON", + Platform: "dappnode", + Timestamp: `{bad json}`, + }, + secretKey: secretKey, expectedCode: http.StatusBadRequest, + tag: types.Solo, }, { - description: "Invalid tag", - payload: validPayload, - pubkey: publicKeyHex, - signature: validSignature, - tag: "invalidTag", + description: "Invalid tag", + payload: types.DecodedPayload{ + Type: "PROOF_OF_VALIDATION", + Platform: "dappnode", + Timestamp: strconv.FormatInt(currentTime-10*24*60*60*1000, 10), + }, + secretKey: secretKey, expectedCode: http.StatusBadRequest, + tag: "invalidTag", }, } - // Execute tests - for _, tc := range tests { + // wait some seconds to ensure the server is ready + time.Sleep(10 * time.Second) + + // Execute test cases + for _, tc := range testCases { + sigHex, payloadBase64 := generateSignature(tc.payload, tc.secretKey) + if tc.customSig != "" { + sigHex = tc.customSig + } + pubKeyHex := "0x" + tc.secretKey.GetPublicKey().SerializeToHexStr() + if tc.invalidKey { + pubKeyHex = "0xinvalidKey" + } + t.Run(tc.description, func(t *testing.T) { e.POST("/signatures"). WithQuery("network", "mainnet"). WithJSON([]types.SignatureRequest{{ - Payload: tc.payload, - Pubkey: tc.pubkey, - Signature: tc.signature, + Payload: payloadBase64, + Pubkey: pubKeyHex, + Signature: sigHex, Tag: tc.tag, }}). Expect(). Status(tc.expectedCode) }) } + + // wait some seconds to ensure all the signatures are processed + time.Sleep(15 * time.Second) + + t.Run("Fetch Signatures with JWT", func(t *testing.T) { + response := e.GET("/signatures"). + WithHeader("Authorization", "Bearer "+jwtToken). + Expect(). + Status(http.StatusOK). + JSON().Array() + + // // Convert the response to a pretty JSON format. Useful for debugging + // jsonResponse, err := json.MarshalIndent(response.Raw(), "", " ") + // if err != nil { + // t.Fatalf("Failed to marshal JSON: %v", err) + // } + // fmt.Printf("Response: %s\n", string(jsonResponse)) + + // Verify that the response contains two elements + response.Length().IsEqual(2) + + // Verify that the first element has pubkey corresponding to secretKey + response.Value(0).Object().Value("pubkey").String().IsEqual("0x" + secretKey.GetPublicKey().SerializeToHexStr()) + + // Verify that the second element has pubkey corresponding to secretKey2 + response.Value(1).Object().Value("pubkey").String().IsEqual("0x" + secretKey2.GetPublicKey().SerializeToHexStr()) + }) }