Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validateDDO: getValidationSignature only for authentificated requests #835

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ jobs:
P2P_ENABLE_UPNP: 'false'
P2P_ENABLE_AUTONAT: 'false'
ALLOWED_ADMINS: '["0xe2DD09d719Da89e5a3D0F2549c7E24566e947260"]'
AUTHORIZED_PUBLISHERS: '["0xe2DD09d719Da89e5a3D0F2549c7E24566e947260","0x529043886F21D9bc1AE0feDb751e34265a246e47"]'
DB_TYPE: 'elasticsearch'
MAX_REQ_PER_MINUTE: 320
MAX_CONNECTIONS_PER_MINUTE: 320
Expand All @@ -278,7 +279,7 @@ jobs:
with:
repository: 'oceanprotocol/ocean-cli'
path: 'ocean-cli'
ref: 'main'
ref: 'issue-authorized-publisher'
- name: Setup Ocean CLI
working-directory: ${{ github.workspace }}/ocean-cli
run: |
Expand Down
7 changes: 7 additions & 0 deletions src/@types/DDO/Nft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ export interface Nft {
owner?: string
created?: string
}

export interface NftRoles {
manager: boolean
deployERC20: boolean
updateMetadata: boolean
store: boolean
}
3 changes: 3 additions & 0 deletions src/@types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export interface FindDDOCommand extends DDOCommand {
// https://github.com/oceanprotocol/ocean-node/issues/47
export interface ValidateDDOCommand extends Command {
ddo: DDO
publisherAddress?: string
signature?: string
nonce?: string
}

export interface StatusCommand extends Command {}
Expand Down
142 changes: 137 additions & 5 deletions src/components/core/handler/ddoHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
} from '../utils/findDdoHandler.js'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../../utils/logging/Logger.js'
import { sleep, readStream } from '../../../utils/util.js'
import { sleep, readStream, isDefined } from '../../../utils/util.js'
import { DDO } from '../../../@types/DDO/DDO.js'
import { CORE_LOGGER } from '../../../utils/logging/common.js'
import { Blockchain } from '../../../utils/blockchain.js'
import { Blockchain, getBlockchainHandler } from '../../../utils/blockchain.js'
import { ethers, isAddress } from 'ethers'
import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' assert { type: 'json' }
// import lzma from 'lzma-native'
import lzmajs from 'lzma-purejs-requirejs'
import {
getNftPermissions,
getValidationSignature,
makeDid,
validateObject
Expand All @@ -43,6 +44,11 @@
wasNFTDeployedByOurFactory
} from '../../Indexer/utils.js'
import { validateDDOHash } from '../../../utils/asset.js'
import { checkNonce } from '../utils/nonceHandler.js'
import {
checkCredentialOnAccessList,
existsAccessListConfigurationForChain
} from '../../../utils/credentials.js'

const MAX_NUM_PROVIDERS = 5
// after 60 seconds it returns whatever info we have available
Expand Down Expand Up @@ -239,7 +245,7 @@
try {
encryptedDocument = ethers.getBytes(task.encryptedDocument)
flags = Number(task.flags)
documentHash = task.documentHash

Check warning on line 248 in src/components/core/handler/ddoHandler.ts

View workflow job for this annotation

GitHub Actions / lint

Use object destructuring
} catch (error) {
CORE_LOGGER.logMessage(`Decrypt DDO: error ${error}`, true)
return {
Expand Down Expand Up @@ -803,10 +809,136 @@
status: { httpStatus: 400, error: `Validation error: ${validation[1]}` }
}
}
const signature = await getValidationSignature(JSON.stringify(task.ddo))

// command contains optional parameter publisherAddress
// command contains optional parameter nonce and nonce is valid for publisherAddress
// command contains optional parameter signature which is the signed message based on nonce by publisherAddress
// ddo.nftAddress exists and it's valid (done above on validateObject())
// publisherAddress has updateMetadata role on ddo.nftAddress contract
// publisherAddress has publishing rights on this node (see #815) (TODO needs other PR merged first)

if (task.publisherAddress && task.nonce && task.signature) {
const nonceDB = this.getOceanNode().getDatabase().nonce
const nonceValid = await checkNonce(
nonceDB,
task.publisherAddress,
Number(task.nonce),
task.signature,
task.ddo.id + task.nonce
)

if (!nonceValid.valid) {
// BAD NONCE OR SIGNATURE
return {
stream: null,
status: { httpStatus: 403, error: 'Invalid nonce' }
}
}

const chain = String(task.ddo.chainId)
// has publishing rights on this node?
const { authorizedPublishers, authorizedPublishersList, supportedNetworks } =
await getConfiguration()
const validChain = isDefined(supportedNetworks[chain])
// first check if chain is valid
if (validChain) {
const blockChain = getBlockchainHandler(supportedNetworks[chain])

// check also NFT permissions
const hasUpdateMetadataPermissions = await (
await getNftPermissions(
blockChain.getSigner(),
task.ddo.nftAddress,
ERC721Template.abi,
task.publisherAddress
)
).updateMetadata
console.log('hasUpdateMetadataPermissions:', hasUpdateMetadataPermissions)

if (!hasUpdateMetadataPermissions) {
// Has no update metadata permissions
return {
stream: null,
status: {
httpStatus: 400,
error: `Validation error: Publisher: ${task.publisherAddress} does not have "updateMetadata" permissions`
}
}
}

let hasPublisherRights = false

// 1 ) check if publisher address is part of AUTHORIZED_PUBLISHERS
const isAuthorizedPublisher =
authorizedPublishers.length > 0 &&
authorizedPublishers.filter(
(publisher) =>
publisher.toLowerCase() === task.publisherAddress.toLowerCase()
).length > 0

if (isAuthorizedPublisher) {
hasPublisherRights = true
} else {
// 2 ) check if there is an access list for this chain: AUTHORIZED_PUBLISHERS_LIST
const existsAccessList = existsAccessListConfigurationForChain(
authorizedPublishersList,
chain
)
if (existsAccessList) {
// check access list contracts
hasPublisherRights = await checkCredentialOnAccessList(
authorizedPublishersList,
chain,
task.publisherAddress,
await blockChain.getSigner()
)
}
}

if (!hasPublisherRights) {
return {
stream: null,
status: {
httpStatus: 400,
error: `Validation error: publisher address is invalid for this node`
}
}
}
} else {
// the chain is not supported, so we can't validate on this node
return {
stream: null,
status: {
httpStatus: 400,
error: `Validation error: DDO chain is invalid for this node`
}
}
}

// ALL GOOD - ADD SIGNATURE
const signature = await getValidationSignature(JSON.stringify(task.ddo))
return {
stream: Readable.from(JSON.stringify(signature)),
status: { httpStatus: 200 }
}
}
// Missing signature, nonce or publisher address
// DDO is a valid object, but we cannot verify the signatures
// const msg =
// 'Partial validation: DDO is valid, but none of "publisher address", "signature" or "nonce" are present. Cannot add validation signature'
// return {
// stream: Readable.from(JSON.stringify(msg)),
// status: {
// httpStatus: 200,
// error: msg
// }
// }
return {
stream: Readable.from(JSON.stringify(signature)),
status: { httpStatus: 200 }
stream: null,
status: {
httpStatus: 400,
error: `Validation error: Either publisher address is missing or there is an invalid signature/nonce`
}
}
} catch (error) {
CORE_LOGGER.logMessageWithEmoji(
Expand Down
3 changes: 3 additions & 0 deletions src/components/core/utils/nonceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export async function checkNonce(
// get nonce from db
let previousNonce = 0 // if none exists
const existingNonce = await db.retrieve(consumer)
console.log('existing:', existingNonce)
console.log('task:', nonce)
if (existingNonce && existingNonce.nonce !== null) {
previousNonce = existingNonce.nonce
}
Expand All @@ -118,6 +120,7 @@ export async function checkNonce(
signature,
message // String(ddoId + nonce)
)
console.log('validate: ', validate)
if (validate.valid) {
const updateStatus = await updateNonce(db, consumer, nonce)
return updateStatus
Expand Down
15 changes: 14 additions & 1 deletion src/components/core/utils/validateDdoHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import SHACLValidator from 'rdf-validate-shacl'
import formats from '@rdfjs/formats-common'
import { fromRdf } from 'rdf-literal'
import { createHash } from 'crypto'
import { ethers, getAddress } from 'ethers'
import { ethers, getAddress, Signer } from 'ethers'
import { CORE_LOGGER } from '../../../utils/logging/common.js'
import { create256Hash } from '../../../utils/crypt.js'
import { getProviderWallet } from './feesHandler.js'
import { Readable } from 'stream'
import { NftRoles } from '../../../@types/DDO/Nft.js'
import { getContract } from '../../../utils/blockchain.js'

const CURRENT_VERSION = '4.7.0'
const ALLOWED_VERSIONS = ['4.1.0', '4.3.0', '4.5.0', '4.7.0']
Expand Down Expand Up @@ -157,3 +159,14 @@ export async function getValidationSignature(ddo: string): Promise<any> {
return { hash: '', publicKey: '', r: '', s: '', v: '' }
}
}

export async function getNftPermissions(
signer: Signer,
nftAddress: string, // smart contract address
nftAbi: any, // smart contract ABI
addressToCheck: string // user account address
): Promise<NftRoles> {
const nftContract = getContract(nftAddress, nftAbi, signer)
const roles = await nftContract.getPermissions(addressToCheck)
return roles
}
21 changes: 12 additions & 9 deletions src/components/httpRoutes/aquarius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FindDdoHandler, ValidateDDOHandler } from '../core/handler/ddoHandler.j
import { QueryDdoStateHandler, QueryHandler } from '../core/handler/queryHandler.js'
import { HTTP_LOGGER } from '../../utils/logging/common.js'
import { DDO } from '../../@types/DDO/DDO.js'
import { QueryCommand } from '../../@types/commands.js'
import { QueryCommand, ValidateDDOCommand } from '../../@types/commands.js'
import { DatabaseFactory } from '../database/DatabaseFactory.js'
import { SearchQuery } from '../../@types/DDO/SearchQuery.js'
import { getConfiguration } from '../../utils/index.js'
Expand Down Expand Up @@ -139,18 +139,21 @@ aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req,
res.status(400).send('Missing DDO object')
return
}
const ddo = JSON.parse(req.body) as DDO

if (!ddo.version) {
res.status(400).send('Missing DDO version')
const data: any = JSON.parse(req.body)
if (!data.ddo || !data.ddo.version) {
res.status(400).send('Invalid DDO data or missing DDO version')
return
}
const task: ValidateDDOCommand = {
ddo: data.ddo as DDO,
nonce: data.nonce,
publisherAddress: data.publisherAddress,
signature: data.signature,
command: PROTOCOL_COMMANDS.VALIDATE_DDO
}

const node = req.oceanNode
const result = await new ValidateDDOHandler(node).handle({
ddo,
command: PROTOCOL_COMMANDS.VALIDATE_DDO
})
const result = await new ValidateDDOHandler(node).handle(task)
if (result.stream) {
const validationResult = JSON.parse(await streamToString(result.stream as Readable))
res.json(validationResult)
Expand Down
3 changes: 2 additions & 1 deletion src/test/integration/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { GetDdoHandler } from '../../components/core/handler/ddoHandler.js'

import { Readable } from 'stream'
import { OceanNodeConfig } from '../../@types/OceanNode.js'
import { getContract } from '../../utils/blockchain.js'

import {
DEFAULT_TEST_TIMEOUT,
Expand All @@ -55,7 +56,7 @@ import { ganachePrivateKeys } from '../utils/addresses.js'
import { homedir } from 'os'
import AccessListFactory from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessListFactory.sol/AccessListFactory.json' assert { type: 'json' }
import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' assert { type: 'json' }
import { deployAccessListContract, getContract } from '../utils/contracts.js'
import { deployAccessListContract } from '../utils/contracts.js'

describe('Should run a complete node flow.', () => {
let config: OceanNodeConfig
Expand Down
14 changes: 2 additions & 12 deletions src/test/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import { Contract, ethers, Signer } from 'ethers'

/**
* Returns a contract instance for the given address
* @param {string} address - The address of the contract
* @param {AbiItem[]} [abi] - The ABI of the contract
* @returns {Contract} - The contract instance
*/
export function getContract(address: string, abi: any, signer: Signer): Contract {
const contract = new ethers.Contract(address, abi, signer)
return contract
}
import { Signer } from 'ethers'
import { getContract } from '../../utils/blockchain.js'

export function getEventFromTx(txReceipt: { logs: any[] }, eventName: string) {
return txReceipt?.logs?.filter((log) => {
Expand Down
24 changes: 23 additions & 1 deletion src/utils/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { getConfiguration } from './config.js'
import { CORE_LOGGER } from './logging/common.js'
import { sleep } from './util.js'
import { ConnectionStatus } from '../@types/blockchain.js'
import { ConnectionStatus, SupportedNetwork } from '../@types/blockchain.js'
import { ValidateChainId } from '../@types/commands.js'

export class Blockchain {
Expand Down Expand Up @@ -218,3 +218,25 @@ export async function getJsonRpcProvider(
}
return new JsonRpcProvider(checkResult.networkRpc)
}

// useful for getting a Blockchain instance, as we repeat this piece of code often
export function getBlockchainHandler(network: SupportedNetwork): Blockchain {
const blockChain = new Blockchain(
network.rpc,
network.network,
network.chainId,
network.fallbackRPCs
)
return blockChain
}

/**
* Returns a contract instance for the given address
* @param {string} address - The address of the contract
* @param {AbiItem[]} [abi] - The ABI of the contract
* @returns {Contract} - The contract instance
*/
export function getContract(address: string, abi: any, signer: Signer): Contract {
const contract = new ethers.Contract(address, abi, signer)
return contract
}
Loading
Loading