Skip to content

Commit

Permalink
Adding js/ts util method to verify JWT's generated by a scanner (#212)
Browse files Browse the repository at this point in the history
* Adding js/ts util method to verify JWT's generated by a scanner

* Updating variable names and adding polygon rpc

* Renaming to remove redundant name

* Making sure to export correct method names

* Adding param to input polygon url + checking exp of JWT

* Adding verify method to python sdk

* updating import

Co-authored-by: Robert Leonard <robertleonard@Roberts-MacBook-Pro-2.local>
  • Loading branch information
Robert-H-Leonard and Robert Leonard committed Sep 7, 2022
1 parent 156bfb3 commit dd5a8ff
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 11 deletions.
2 changes: 1 addition & 1 deletion python-sdk/src/forta_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .trace import Trace, TraceAction, TraceResult
from .event_type import EventType
from .network import Network
from .utils import get_json_rpc_url, create_block_event, create_transaction_event, get_web3_provider, keccak256, get_transaction_receipt, get_alerts, fetch_Jwt_token, decode_Jwt_token
from .utils import get_json_rpc_url, create_block_event, create_transaction_event, get_web3_provider, keccak256, get_transaction_receipt, get_alerts, fetch_jwt, decode_jwt, verify_jwt
from web3 import Web3

web3Provider = Web3(Web3.HTTPProvider(get_json_rpc_url()))
68 changes: 65 additions & 3 deletions python-sdk/src/forta_agent/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
import sha3
import requests
import datetime
import time
from web3.auto import w3
from web3 import Web3
import json
import logging

from .forta_graphql import AlertsResponse

DISPTACHER_ABI = [{"inputs":[{"internalType":"uint256","name":"agentId","type":"uint256"},{"internalType":"uint256","name":"scannerId","type":"uint256"}],"name":"areTheyLinked","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]
DISPATCH_CONTRACT = "0xd46832F3f8EA8bDEFe5316696c0364F01b31a573"; # Source: https://docs.forta.network/en/latest/smart-contracts/

def get_web3_provider():
from . import web3Provider
Expand Down Expand Up @@ -130,7 +137,7 @@ def keccak256(val):
hash.update(bytes(val, encoding='utf-8'))
return f'0x{hash.hexdigest()}'

def fetch_Jwt_token(claims, expiresAt=None) -> str:
def fetch_jwt(claims, expiresAt=None) -> str:
host_name = 'forta-jwt-provider'
port = 8515
path = '/create'
Expand Down Expand Up @@ -162,7 +169,62 @@ def fetch_Jwt_token(claims, expiresAt=None) -> str:
else:
raise err

def verify_jwt(token: str, polygonUrl: str ='https://polygon-rpc.com') -> bool:
splitJwt = token.split('.')
rawHeader = splitJwt[0]
rawPayload = splitJwt[1]

def decode_Jwt_token(token):
header = json.loads(base64.urlsafe_b64decode(rawHeader + '==').decode('utf-8'))
payload = json.loads(base64.urlsafe_b64decode(rawPayload + '==').decode('utf-8'))

alg = header['alg']
botId = payload['bot-id']
expiresAt = payload['exp']
signerAddress = payload['sub']

if (signerAddress is None) or (botId is None):
logging.warning('Invalid claim')
return False

if alg != 'ETH':
logging.warning('Unexpected signing method: {alg}'.format(alg=alg))
return False

currentUnixTime = time.mktime(datetime.datetime.utcnow().utctimetuple())

if expiresAt < currentUnixTime:
logging.warning('Jwt expired')
return False

msg = '{header}.{payload}'.format(header=rawHeader, payload=rawPayload)
msgHash = w3.keccak(text=msg)
b64signature = splitJwt[2]
signature = base64.urlsafe_b64decode(f'{b64signature}=').hex()
recoveredSignerAddress = w3.eth.account.recoverHash(msgHash, signature=signature)

if recoveredSignerAddress != signerAddress:
logging.warn('Signature invalid: expected={signerAddress}, got={recoveredSignerAddress}'.format(signerAddress=signerAddress, recoveredSignerAddress=recoveredSignerAddress))
return False

w3Client = Web3(Web3.HTTPProvider(polygonUrl))
contract = w3Client.eth.contract(address=DISPATCH_CONTRACT,abi=DISPTACHER_ABI)

areTheyLinked = contract.functions.areTheyLinked(int(botId,0), int(recoveredSignerAddress,0)).call()

return areTheyLinked

class DecodedJwt:
def __init__(self, dict):
self.header = dict.get('header')
self.payload = dict.get('payload')


def decode_jwt(token):
# Adding need 4 byte for pythons b64decode
return base64.b64decode(token.split('.')[1] + '==').decode('utf-8')
header = base64.urlsafe_b64decode(token.split('.')[0] + '==').decode('utf-8')
payload = base64.urlsafe_b64decode(token.split('.')[1] + '==').decode('utf-8')

return DecodedJwt({
"header": header,
"payload": payload
})
10 changes: 6 additions & 4 deletions sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
isPrivateFindings,
getTransactionReceipt,
getAlerts,
fetchJwtToken,
decodeJwtToken
fetchJwt,
decodeJwt,
verifyJwt
} from "./utils"
import awilixConfigureContainer from '../cli/di.container';

Expand Down Expand Up @@ -103,6 +104,7 @@ export {
configureContainer,
getTransactionReceipt,
getAlerts,
fetchJwtToken,
decodeJwtToken
fetchJwt,
decodeJwt,
verifyJwt
}
77 changes: 74 additions & 3 deletions sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Log, Receipt } from './receipt'
import { TxEventBlock } from './transaction.event'
import { Block } from './block'
import { ethers } from '.'
import { toUtf8Bytes } from "@ethersproject/strings"
import { AlertQueryOptions, AlertsResponse, FORTA_GRAPHQL_URL, getQueryFromAlertOptions, RawGraphqlAlertResponse } from './graphql/forta'
import axios from 'axios'

Expand Down Expand Up @@ -154,7 +155,7 @@ export const getAlerts = async (query: AlertQueryOptions): Promise<AlertsRespons
return response.data.data.alerts
}

export const fetchJwtToken = async (claims: {}, expiresAt?: Date): Promise<{token: string} | null> => {
export const fetchJwt = async (claims: {}, expiresAt?: Date): Promise<{token: string} | null> => {
const hostname = 'forta-jwt-provider'
const port = 8515
const path = '/create'
Expand Down Expand Up @@ -188,6 +189,76 @@ export const fetchJwtToken = async (claims: {}, expiresAt?: Date): Promise<{toke
}
}

export const decodeJwtToken = (token: string) => {
return JSON.parse(Buffer.from((token as string).split('.')[1], 'base64').toString())
interface DecodedJwt {
header: any,
payload: any
}

export const decodeJwt = (token: string): DecodedJwt => {

const splitJwt = (token).split('.');
const header = JSON.parse(Buffer.from(splitJwt[0], 'base64').toString())
const payload = JSON.parse(Buffer.from(splitJwt[1], 'base64').toString())

return {
header,
payload
}
}

const DISPTACHER_ARE_THEY_LINKED = "function areTheyLinked(uint256 agentId, uint256 scannerId) external view returns(bool)";
const DISPATCH_CONTRACT = "0xd46832F3f8EA8bDEFe5316696c0364F01b31a573"; // Source: https://docs.forta.network/en/latest/smart-contracts/

export const verifyJwt = async (token: string, polygonRpcUrl: string = "https://polygon-rpc.com"): Promise<boolean> => {
const splitJwt = (token).split('.')
const rawHeader = splitJwt[0]
const rawPayload = splitJwt[1]

const header = JSON.parse(Buffer.from(rawHeader, 'base64').toString())
const payload = JSON.parse(Buffer.from(rawPayload, 'base64').toString())

const botId = payload["bot-id"] as string
const expiresAt = payload["exp"] as number
const algorithm = header?.alg;

if(algorithm !== "ETH") {
console.warn(`Unexpected signing method: ${algorithm}`)
return false
}

if(!botId) {
console.warn(`Invalid claim`)
return false
}

const signerAddress = payload?.sub as string | undefined // public key should be contract address that signed the JWT

if(!signerAddress) {
console.warn(`Invalid claim`)
return false
}

const currentUnixTime = Math.floor((Date.now() / 1000))

if(expiresAt < currentUnixTime) {
console.warn(`Jwt is expired`)
return false
}

const digest = ethers.utils.keccak256(toUtf8Bytes(`${rawHeader}.${rawPayload}`))
const signature = `0x${ Buffer.from(splitJwt[2], 'base64').toString('hex')}`

const recoveredSignerAddress = ethers.utils.recoverAddress(digest, signature) // Contract address that signed message

if(recoveredSignerAddress !== signerAddress) {
console.warn(`Signature invalid: expected=${signerAddress}, got=${recoveredSignerAddress}`)
return false
}

const polygonProvider = new ethers.providers.JsonRpcProvider(polygonRpcUrl)

const dispatchContract = new ethers.Contract(DISPATCH_CONTRACT, [DISPTACHER_ARE_THEY_LINKED], polygonProvider)
const areTheyLinked = await dispatchContract.areTheyLinked(botId, recoveredSignerAddress)

return areTheyLinked
}

0 comments on commit dd5a8ff

Please sign in to comment.