Skip to content

Commit

Permalink
Validate Attestation Requests (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
nambrot authored and celo-ci-bot-user committed Oct 30, 2019
1 parent 578a8e2 commit 51c359d
Show file tree
Hide file tree
Showing 18 changed files with 403 additions and 82 deletions.
2 changes: 1 addition & 1 deletion packages/attestation-service/config/.env.development
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DB_URL=sqlite://db/dev.db
DATABASE_URL=sqlite://db/dev.db
CELO_PROVIDER=https://integration-forno.celo-testnet.org
ACCOUNT_ADDRESS=0xE6e53b5fc2e18F51781f14a3ce5E7FD468247a15
ATTESTATION_KEY=x
Expand Down
6 changes: 3 additions & 3 deletions packages/attestation-service/config/database.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"development": {
"use_env_variable": "DB_URL"
"use_env_variable": "DATABASE_URL"
},
"staging": {
"use_env_variable": "DB_URL"
"use_env_variable": "DATABASE_URL"
},
"production": {
"use_env_variable": "DB_URL"
"use_env_variable": "DATABASE_URL"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict'
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()

try {
await queryInterface.createTable('Attestations', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
account: {
allowNull: false,
type: Sequelize.STRING,
},
phoneNumber: {
allowNull: false,
type: Sequelize.STRING,
},
issuer: {
allowNull: false,
type: Sequelize.STRING,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
})

await queryInterface.addIndex(
'Attestations',
['account', 'phoneNumber', 'issuer'],
{ fields: ['account', 'phoneNumber', 'issuer'], unique: true },
{ transaction }
)

await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Attestations')
},
}
9 changes: 7 additions & 2 deletions packages/attestation-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,29 @@
"clean": "tsc -b . --clean",
"clean:all": "yarn clean && rm -rf lib",
"prepublishOnly": "yarn build:gen && yarn build",
"start": "TS_NODE_FILES=true ts-node src/index.ts",
"start-ts": "TS_NODE_FILES=true ts-node src/index.ts",
"start": "node lib/index.js",
"db:create:dev": "mkdir -p db && touch db/dev.db",
"db:migrate": "sequelize db:migrate",
"db:migrate:undo": "sequelize db:migrate:undo",
"db:migrate:dev": "sequelize db:migrate",
"dev": "CONFIG=config/.env.development nodemon",
"lint": "tslint -c tslint.json --project ."
},
"dependencies": {
"@celo/contractkit": "0.1.1",
"@celo/contractkit": "0.1.6",
"@celo/utils": "^0.1.0",
"bignumber.js": "^7.2.0",
"body-parser": "1.19.0",
"debug": "^4.1.1",
"dotenv": "8.0.0",
"eth-lib": "^0.2.8",
"io-ts": "2.0.1",
"fp-ts":"2.1.1",
"nexmo": "2.4.2",
"web3": "1.0.0-beta.37",
"express": "4.17.1",
"mysql2": "2.0.0-alpha1",
"pg": "7.12.1",
"pg-hstore": "2.3.3",
"sequelize": "5.13.1",
Expand Down
114 changes: 100 additions & 14 deletions packages/attestation-service/src/attestation.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,124 @@
import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations'
import { attestToIdentifier, SignatureUtils } from '@celo/utils'
import { privateKeyToAddress } from '@celo/utils/lib/address'
import { retryAsyncWithBackOff } from '@celo/utils/lib/async'
import express from 'express'
import * as t from 'io-ts'
import { existingAttestationRequest, kit, persistAttestationRequest } from './db'
import { Address, AddressType, E164Number, E164PhoneNumberType } from './request'
import { sendSms } from './sms'
function signAttestation(phoneNumber: string, account: string) {

export const AttestationRequestType = t.type({
phoneNumber: E164PhoneNumberType,
account: AddressType,
issuer: AddressType,
})

export type AttestationRequest = t.TypeOf<typeof AttestationRequestType>

function getAttestationKey() {
if (process.env.ATTESTATION_KEY === undefined) {
console.error('Did not specify ATTESTATION_KEY')
throw new Error('Did not specify ATTESTATION_KEY')
}

const signature = attestToIdentifier(phoneNumber, account, process.env.ATTESTATION_KEY)
return process.env.ATTESTATION_KEY
}

async function validateAttestationRequest(request: AttestationRequest) {
// check if it exists in the database
if (
(await existingAttestationRequest(request.phoneNumber, request.account, request.issuer)) !==
null
) {
throw new Error('Attestation already sent')
}
const key = getAttestationKey()
const address = privateKeyToAddress(key)

// TODO: Check with the new Accounts.sol
if (address.toLowerCase() !== request.issuer.toLowerCase()) {
throw new Error(`Mismatching issuer, I am ${address}`)
}

const attestations = await kit.contracts.getAttestations()
const state = await attestations.getAttestationState(
request.phoneNumber,
request.account,
request.issuer
)

if (state.attestationState !== AttestationState.Incomplete) {
throw new Error('No incomplete attestation found')
}

// TODO: Check expiration
return
}

async function validateAttestation(
attestationRequest: AttestationRequest,
attestationCode: string
) {
const attestations = await kit.contracts.getAttestations()
const isValid = await attestations.validateAttestationCode(
attestationRequest.phoneNumber,
attestationRequest.account,
attestationRequest.issuer,
attestationCode
)
if (!isValid) {
throw new Error('Valid attestation could not be provided')
}
return
}

function signAttestation(phoneNumber: E164Number, account: Address) {
const signature = attestToIdentifier(phoneNumber, account, getAttestationKey())

return SignatureUtils.serializeSignature(signature)
}

function toBase64(str: string) {
return Buffer.from(str, 'hex').toString('base64')
return Buffer.from(str.slice(2), 'hex').toString('base64')
}

function createAttestationTextMessage(attestationCode: string) {
return `<#> ${toBase64(attestationCode)} ${process.env.APP_SIGNATURE}`
}

export async function handleAttestationRequest(req: express.Request, res: express.Response) {
// TODO: Should parse request appropriately

// TODO: Should validate request here
// const attestations = await kit.contracts.getAttestations()

// Produce attestation
const attestationCode = signAttestation(req.body.phoneNumber, req.body.account)
const textMessage = createAttestationTextMessage(attestationCode)
export async function handleAttestationRequest(
_req: express.Request,
res: express.Response,
attestationRequest: AttestationRequest
) {
let attestationCode
try {
await validateAttestationRequest(attestationRequest)
attestationCode = signAttestation(attestationRequest.phoneNumber, attestationRequest.account)
await validateAttestation(attestationRequest, attestationCode)
} catch (error) {
console.error(error)
res.status(422).json({ success: false, error: error.toString() })
return
}

// Send the SMS
await retryAsyncWithBackOff(sendSms, 10, [req.body.phoneNumber, textMessage], 1000)
try {
const textMessage = createAttestationTextMessage(attestationCode)
await persistAttestationRequest(
attestationRequest.phoneNumber,
attestationRequest.account,
attestationRequest.issuer
)
await retryAsyncWithBackOff(sendSms, 10, [attestationRequest.phoneNumber, textMessage], 1000)
} catch (error) {
console.error(error)
res.status(500).json({
success: false,
error: 'Something went wrong while attempting to send SMS, try again later',
})
return
}

res.json({ success: true })
}
30 changes: 28 additions & 2 deletions packages/attestation-service/src/db.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ContractKit, newKit } from '@celo/contractkit'
import { Sequelize } from 'sequelize'
import { fetchEnv } from './env'
import Attestation, { AttestationStatic } from './models/attestation'

export let sequelize: Sequelize | undefined

export function initializeDB() {
if (sequelize === undefined) {
sequelize = new Sequelize(fetchEnv('DB_URL'))
sequelize = new Sequelize(fetchEnv('DATABASE_URL'))
return sequelize.authenticate() as Promise<void>
}

return Promise.resolve()
}

Expand All @@ -20,3 +20,29 @@ export function initializeKit() {
kit = newKit(fetchEnv('CELO_PROVIDER'))
}
}

let AttestationTable: AttestationStatic

async function getAttestationTable() {
if (AttestationTable) {
return AttestationTable
}
AttestationTable = await Attestation(sequelize!)
return AttestationTable
}

export async function existingAttestationRequest(
phoneNumber: string,
account: string,
issuer: string
): Promise<AttestationStatic | null> {
return (await getAttestationTable()).findOne({ where: { phoneNumber, account, issuer } })
}

export async function persistAttestationRequest(
phoneNumber: string,
account: string,
issuer: string
) {
return (await getAttestationTable()).create({ phoneNumber, account, issuer })
}
8 changes: 6 additions & 2 deletions packages/attestation-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as dotenv from 'dotenv'
import express from 'express'
import { handleAttestationRequest } from './attestation'
import { AttestationRequestType, handleAttestationRequest } from './attestation'
import { initializeDB, initializeKit } from './db'
import { createValidatedHandler } from './request'
import { initializeSmsProviders } from './sms'

async function init() {
Expand All @@ -18,7 +19,10 @@ async function init() {
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Server running on ${port}!`))

app.post('/attestations', handleAttestationRequest)
app.post(
'/attestations',
createValidatedHandler(AttestationRequestType, handleAttestationRequest)
)
}

init().catch((err) => {
Expand Down
18 changes: 18 additions & 0 deletions packages/attestation-service/src/models/attestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BuildOptions, DataTypes, Model, Sequelize } from 'sequelize'

interface AttestationModel extends Model {
readonly id: number
account: string
phoneNumber: string
issuer: string
}

export type AttestationStatic = typeof Model &
(new (values?: object, options?: BuildOptions) => AttestationModel)

export default (sequelize: Sequelize) =>
sequelize.define('Attestations', {
account: DataTypes.STRING,
phoneNumber: DataTypes.STRING,
issuer: DataTypes.STRING,
}) as AttestationStatic
Loading

0 comments on commit 51c359d

Please sign in to comment.