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

Add AccountClaim to Metadata #1663

Merged
merged 8 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion packages/attestation-service/src/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ 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 { Address, AddressType, E164Number, E164PhoneNumberType } from '@celo/utils/lib/io'
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'

export const AttestationRequestType = t.type({
Expand Down
35 changes: 1 addition & 34 deletions packages/attestation-service/src/request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { isE164NumberStrict } from '@celo/utils/lib/phoneNumbers'
import { isValidAddress } from '@celo/utils/lib/signatureUtils'
import express from 'express'
import { either, isLeft } from 'fp-ts/lib/Either'
import { isLeft } from 'fp-ts/lib/Either'
import * as t from 'io-ts'

export function createValidatedHandler<T>(
Expand All @@ -27,37 +25,6 @@ export function createValidatedHandler<T>(
}
}

export const E164PhoneNumberType = new t.Type<string, string, unknown>(
'E164Number',
t.string.is,
(input, context) =>
either.chain(
t.string.validate(input, context),
(stringValue) =>
isE164NumberStrict(stringValue)
? t.success(stringValue)
: t.failure(stringValue, context, 'is not a valid e164 number')
),
String
)

export const AddressType = new t.Type<string, string, unknown>(
'Address',
t.string.is,
(input, context) =>
either.chain(
t.string.validate(input, context),
(stringValue) =>
isValidAddress(stringValue)
? t.success(stringValue)
: t.failure(stringValue, context, 'is not a valid address')
),
String
)

export type Address = t.TypeOf<typeof AddressType>
export type E164Number = t.TypeOf<typeof E164PhoneNumberType>

function serializeErrors(errors: t.Errors) {
let serializedErrors: any = {}
errors.map((error) => {
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/commands/account/claim-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createAccountClaim } from '@celo/contractkit/lib/identity/claims/account'
import { flags } from '@oclif/command'
import { ClaimCommand } from '../../utils/identity'

export default class ClaimAccount extends ClaimCommand {
static description = 'Claim another account in a local metadata file'
static flags = {
...ClaimCommand.flags,
address: flags.string({
required: true,
description: 'The address of the account you want to claim',
}),
publicKey: flags.string({
default: undefined,
description: 'The public key of the account if you want others to encrypt messages to you',
}),
}
static args = ClaimCommand.args
static examples = ['claim-account ~/metadata.json --address test.com --from 0x0']
self = ClaimAccount
async run() {
const res = this.parse(ClaimAccount)
const metadata = this.readMetadata()
await this.addClaim(metadata, createAccountClaim(res.flags.address, res.flags.publicKey))
this.writeMetadata(metadata)
}
}
15 changes: 13 additions & 2 deletions packages/cli/src/commands/account/claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readFileSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import Web3 from 'web3'
import { testWithGanache } from '../../test-utils/ganache-test'
import ClaimAccount from './claim-account'
import ClaimDomain from './claim-domain'
import ClaimName from './claim-name'
import CreateMetadata from './create-metadata'
Expand All @@ -12,9 +13,9 @@ process.env.NO_SYNCCHECK = 'true'

testWithGanache('account:authorize cmd', (web3: Web3) => {
let account: string

let accounts: string[]
beforeEach(async () => {
const accounts = await web3.eth.getAccounts()
accounts = await web3.eth.getAccounts()
account = accounts[0]
})

Expand Down Expand Up @@ -54,6 +55,16 @@ testWithGanache('account:authorize cmd', (web3: Web3) => {
expect(claim).toBeDefined()
expect(claim!.domain).toEqual(domain)
})

test('account:claim-account cmd', async () => {
generateEmptyMetadataFile()
const otherAccount = accounts[1]
await ClaimAccount.run(['--from', account, '--address', otherAccount, emptyFilePath])
const metadata = readFile()
const claim = metadata.findClaim(ClaimTypes.ACCOUNT)
expect(claim).toBeDefined()
expect(claim!.address).toEqual(otherAccount)
})
})

describe('account:register-metadata cmd', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/account/get-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export default class GetMetadata extends BaseCommand {
try {
const metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL)
console.info('Metadata contains the following claims: \n')
await displayMetadata(metadata)
await displayMetadata(metadata, this.kit)
} catch (error) {
console.error('Metadata could not be retrieved from ', metadataURL)
console.error(`Metadata could not be retrieved from ${metadataURL}: ${error.toString()}`)
}
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/commands/account/show-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export default class ShowMetadata extends BaseCommand {
const res = this.parse(ShowMetadata)
const metadata = IdentityMetadataWrapper.fromFile(res.args.file)
console.info(`Metadata at ${res.args.file} contains the following claims: \n`)
await displayMetadata(metadata)
await displayMetadata(metadata, this.kit)
}
}
6 changes: 1 addition & 5 deletions packages/cli/src/utils/command.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { URL_REGEX } from '@celo/utils/lib/io'
import { flags } from '@oclif/command'
import { CLIError } from '@oclif/errors'
import { IArg, ParseFn } from '@oclif/parser/lib/args'
Expand Down Expand Up @@ -28,11 +29,6 @@ const parsePath: ParseFn<string> = (input) => {
}
}

// from http://urlregex.com/
const URL_REGEX = new RegExp(
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/
)

const parseUrl: ParseFn<string> = (input) => {
if (URL_REGEX.test(input)) {
return input
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/utils/identity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ContractKit } from '@celo/contractkit'
import { ClaimTypes, IdentityMetadataWrapper } from '@celo/contractkit/lib/identity'
import { Claim, hashOfClaim, verifyClaim } from '@celo/contractkit/lib/identity/claims/claim'
import { VERIFIABLE_CLAIM_TYPES } from '@celo/contractkit/lib/identity/claims/types'
Expand Down Expand Up @@ -78,10 +79,11 @@ export const claimFlags = {

export const claimArgs = [Args.file('file', { description: 'Path of the metadata file' })]

export const displayMetadata = async (metadata: IdentityMetadataWrapper) => {
export const displayMetadata = async (metadata: IdentityMetadataWrapper, kit: ContractKit) => {
const accounts = await kit.contracts.getAccounts()
const data = await concurrentMap(5, metadata.claims, async (claim) => {
const verifiable = VERIFIABLE_CLAIM_TYPES.includes(claim.payload.type)
const status = await verifyClaim(claim, metadata.data.meta.address)
const status = await verifyClaim(claim, metadata.data.meta.address, accounts.getMetadataURL)
let extra = ''
switch (claim.payload.type) {
case ClaimTypes.ATTESTATION_SERVICE_URL:
Expand All @@ -104,7 +106,7 @@ export const displayMetadata = async (metadata: IdentityMetadataWrapper) => {
type: claim.payload.type,
extra,
verifiable: verifiable ? 'Yes' : 'No',
status: verifiable ? (status ? `Invalid: ${status}` : 'Valid!') : '',
status: verifiable ? (status ? `Invalid: ${status}` : 'Valid!') : 'N/A',
createdAt: moment.unix(claim.payload.timestamp).fromNow(),
hash: hashOfClaim(claim.payload),
}
Expand Down
113 changes: 113 additions & 0 deletions packages/contractkit/src/identity/claims/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address'
import { NativeSigner } from '@celo/utils/lib/signatureUtils'
import { newKitFromWeb3 } from '../../kit'
import { testWithGanache } from '../../test-utils/ganache-test'
import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '../../test-utils/ganache.setup'
import { IdentityMetadataWrapper } from '../metadata'
import { createAccountClaim, MetadataURLGetter } from './account'
import { SignedClaim, verifyClaim } from './claim'

testWithGanache('Account claims', (web3) => {
const kit = newKitFromWeb3(web3)
const address = ACCOUNT_ADDRESSES[0]
const otherAddress = ACCOUNT_ADDRESSES[1]

it('can make an account claim', async () => {
const metadata = IdentityMetadataWrapper.fromEmpty(address)
await metadata.addClaim(
createAccountClaim(otherAddress),
NativeSigner(kit.web3.eth.sign, address)
)
})

it('can make an account claim with the public key', async () => {
const metadata = IdentityMetadataWrapper.fromEmpty(address)
const otherKey = ACCOUNT_PRIVATE_KEYS[1]
await metadata.addClaim(
createAccountClaim(privateKeyToAddress(otherKey), privateKeyToPublicKey(otherKey)),
NativeSigner(kit.web3.eth.sign, address)
)
})

it("can't claim itself", async () => {
const metadata = IdentityMetadataWrapper.fromEmpty(address)
await expect(
metadata.addClaim(createAccountClaim(address), NativeSigner(kit.web3.eth.sign, address))
).rejects.toEqual(new Error("Can't claim self"))
})

it('fails to create a claim with in invalid address', () => {
expect(() => {
createAccountClaim('notanaddress')
}).toThrow()
})

it('fails when passing a public key that is derived from a different private key', async () => {
const key1 = ACCOUNT_PRIVATE_KEYS[0]
const key2 = ACCOUNT_PRIVATE_KEYS[1]

expect(() =>
createAccountClaim(privateKeyToAddress(key1), privateKeyToPublicKey(key2))
).toThrow()
})

describe('verifying', () => {
let signedClaim: SignedClaim
let otherMetadata: IdentityMetadataWrapper
let metadataUrlGetter: MetadataURLGetter

// Mocking static function calls was too difficult, so manually mocking it
const originalFetchFromURLImplementation = IdentityMetadataWrapper.fetchFromURL

beforeEach(async () => {
otherMetadata = IdentityMetadataWrapper.fromEmpty(otherAddress)

const myUrl = 'https://www.test.com/'
metadataUrlGetter = (_addr: string) => Promise.resolve(myUrl)

IdentityMetadataWrapper.fetchFromURL = () => Promise.resolve(otherMetadata)

const metadata = IdentityMetadataWrapper.fromEmpty(address)
signedClaim = await metadata.addClaim(
createAccountClaim(otherAddress),
NativeSigner(kit.web3.eth.sign, address)
)
})

afterEach(() => {
IdentityMetadataWrapper.fetchFromURL = originalFetchFromURLImplementation
})

describe('when the metadata URL of the other account has not been set', () => {
beforeEach(() => {
metadataUrlGetter = (_addr: string) => Promise.resolve('')
})

it('indicates that the metadata url could not be retrieved', async () => {
const error = await verifyClaim(signedClaim, address, metadataUrlGetter)
expect(error).toContain('could not be retrieved')
})
})

describe('when the metadata URL is set, but does not contain the address claim', () => {
it('indicates that the metadata does not contain the counter claim', async () => {
const error = await verifyClaim(signedClaim, address, metadataUrlGetter)
expect(error).toContain('did not claim')
})
})

describe('when the other account correctly counter-claims', () => {
beforeEach(async () => {
await otherMetadata.addClaim(
createAccountClaim(address),
NativeSigner(kit.web3.eth.sign, otherAddress)
)
})

it('returns undefined succesfully', async () => {
const error = await verifyClaim(signedClaim, address, metadataUrlGetter)
expect(error).toBeUndefined()
})
})
})
})
89 changes: 89 additions & 0 deletions packages/contractkit/src/identity/claims/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { AddressType, isValidUrl, PublicKeyType } from '@celo/utils/lib/io'
import { pubToAddress, toChecksumAddress } from 'ethereumjs-util'
import { either, isLeft } from 'fp-ts/lib/Either'
import * as t from 'io-ts'
import { Address } from '../../base'
import { IdentityMetadataWrapper } from '../metadata'
import { ClaimTypes, now, TimestampType } from './types'

// Provide the type minus the validation that the public key and address are derived from the same private key
export const AccountClaimTypeH = t.type({
type: t.literal(ClaimTypes.ACCOUNT),
timestamp: TimestampType,
address: AddressType,
// io-ts way of defining optional key-value pair
publicKey: t.union([t.undefined, PublicKeyType]),
})

export const AccountClaimType = new t.Type<AccountClaim, any, unknown>(
'AccountClaimType',
AccountClaimTypeH.is,
(unknownValue, context) =>
either.chain(AccountClaimTypeH.validate(unknownValue, context), (claim) => {
if (claim.publicKey === undefined) {
return t.success(claim)
}
const derivedAddress = toChecksumAddress(
'0x' + pubToAddress(Buffer.from(claim.publicKey.slice(2), 'hex'), true).toString('hex')
)
return derivedAddress === claim.address
? t.success(claim)
: t.failure(claim, context, 'public key did not match the address in the claim')
}),
(x) => x
)

export type AccountClaim = t.TypeOf<typeof AccountClaimTypeH>

export const createAccountClaim = (address: string, publicKey?: string): AccountClaim => {
const claim = {
timestamp: now(),
type: ClaimTypes.ACCOUNT,
address,
publicKey,
}

const parsedClaim = AccountClaimType.decode(claim)

if (isLeft(parsedClaim)) {
throw new Error(`A valid claim could not be created`)
}

return parsedClaim.right
}

/**
* A function that can asynchronously fetch the metadata URL for an account address
* Should virtually always be Accounts#getMetadataURL
*/
export type MetadataURLGetter = (address: Address) => Promise<string>

export const verifyAccountClaim = async (
claim: AccountClaim,
address: string,
metadataURLGetter: MetadataURLGetter
) => {
const metadataURL = await metadataURLGetter(claim.address)

console.info(JSON.stringify(metadataURL))
if (!isValidUrl(metadataURL)) {
return `Metadata URL of ${claim.address} could not be retrieved`
}

let metadata: IdentityMetadataWrapper
try {
metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL)
} catch (error) {
return `Metadata could not be fetched for ${
claim.address
} at ${metadataURL}: ${error.toString()}`
}

const accountClaims = metadata.filterClaims(ClaimTypes.ACCOUNT)

if (accountClaims.find((x) => x.address === address) === undefined) {
return `${claim.address} did not claim ${address}`
}

return
}
Loading