From 2f8de330a934db1213e79da9394321228745cc64 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 11 Nov 2019 14:58:02 -0800 Subject: [PATCH 1/5] Add account claim --- .../attestation-service/src/attestation.ts | 2 +- packages/attestation-service/src/request.ts | 35 +--------- .../cli/src/commands/account/claim-account.ts | 27 ++++++++ .../cli/src/commands/account/claims.test.ts | 15 ++++- .../src/identity/claims/account.ts | 51 ++++++++++++++ .../contractkit/src/identity/claims/claim.ts | 18 ++++- .../contractkit/src/identity/claims/types.ts | 20 +----- .../contractkit/src/identity/metadata.test.ts | 45 ++++++++++++- packages/contractkit/src/identity/metadata.ts | 13 +++- packages/utils/src/address.ts | 10 ++- packages/utils/src/io.ts | 66 +++++++++++++++++++ 11 files changed, 241 insertions(+), 61 deletions(-) create mode 100644 packages/cli/src/commands/account/claim-account.ts create mode 100644 packages/contractkit/src/identity/claims/account.ts create mode 100644 packages/utils/src/io.ts diff --git a/packages/attestation-service/src/attestation.ts b/packages/attestation-service/src/attestation.ts index 37411ea33f7..d0d1e004496 100644 --- a/packages/attestation-service/src/attestation.ts +++ b/packages/attestation-service/src/attestation.ts @@ -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({ diff --git a/packages/attestation-service/src/request.ts b/packages/attestation-service/src/request.ts index dfd5ae13910..e3d7bd9b6c6 100644 --- a/packages/attestation-service/src/request.ts +++ b/packages/attestation-service/src/request.ts @@ -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( @@ -27,37 +25,6 @@ export function createValidatedHandler( } } -export const E164PhoneNumberType = new t.Type( - '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( - '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 -export type E164Number = t.TypeOf - function serializeErrors(errors: t.Errors) { let serializedErrors: any = {} errors.map((error) => { diff --git a/packages/cli/src/commands/account/claim-account.ts b/packages/cli/src/commands/account/claim-account.ts new file mode 100644 index 00000000000..dd64ed030a7 --- /dev/null +++ b/packages/cli/src/commands/account/claim-account.ts @@ -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) + } +} diff --git a/packages/cli/src/commands/account/claims.test.ts b/packages/cli/src/commands/account/claims.test.ts index 3f993f5841b..df2994703b3 100644 --- a/packages/cli/src/commands/account/claims.test.ts +++ b/packages/cli/src/commands/account/claims.test.ts @@ -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' @@ -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] }) @@ -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', () => { diff --git a/packages/contractkit/src/identity/claims/account.ts b/packages/contractkit/src/identity/claims/account.ts new file mode 100644 index 00000000000..e238ed2be36 --- /dev/null +++ b/packages/contractkit/src/identity/claims/account.ts @@ -0,0 +1,51 @@ +import { AddressType, 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 { 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( + '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 + +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 +} diff --git a/packages/contractkit/src/identity/claims/claim.ts b/packages/contractkit/src/identity/claims/claim.ts index 337b0041348..3e3b7b00514 100644 --- a/packages/contractkit/src/identity/claims/claim.ts +++ b/packages/contractkit/src/identity/claims/claim.ts @@ -1,7 +1,9 @@ +import { JSONStringType } from '@celo/utils/lib/io' import { hashMessage, parseSignature } from '@celo/utils/lib/signatureUtils' import * as t from 'io-ts' +import { AccountClaim, AccountClaimType } from './account' import { KeybaseClaim, KeybaseClaimType, verifyKeybaseClaim } from './keybase' -import { ClaimTypes, JSONStringType, now, SignatureType, TimestampType, UrlType } from './types' +import { ClaimTypes, now, SignatureType, TimestampType, UrlType } from './types' const AttestationServiceURLClaimType = t.type({ type: t.literal(ClaimTypes.ATTESTATION_SERVICE_URL), @@ -23,6 +25,7 @@ const NameClaimType = t.type({ export const ClaimType = t.union([ AttestationServiceURLClaimType, + AccountClaimType, DomainClaimType, KeybaseClaimType, NameClaimType, @@ -41,13 +44,22 @@ export type SignedClaim = t.TypeOf export type AttestationServiceURLClaim = t.TypeOf export type DomainClaim = t.TypeOf export type NameClaim = t.TypeOf -export type Claim = AttestationServiceURLClaim | DomainClaim | KeybaseClaim | NameClaim +export type Claim = + | AttestationServiceURLClaim + | DomainClaim + | KeybaseClaim + | NameClaim + | AccountClaim export type ClaimPayload = K extends typeof ClaimTypes.DOMAIN ? DomainClaim : K extends typeof ClaimTypes.NAME ? NameClaim - : K extends typeof ClaimTypes.KEYBASE ? KeybaseClaim : AttestationServiceURLClaim + : K extends typeof ClaimTypes.KEYBASE + ? KeybaseClaim + : K extends typeof ClaimTypes.ATTESTATION_SERVICE_URL + ? AttestationServiceURLClaim + : AccountClaim export const isOfType = (type: K) => ( data: SignedClaim['payload'] diff --git a/packages/contractkit/src/identity/claims/types.ts b/packages/contractkit/src/identity/claims/types.ts index 525545ab1b0..63a5536a329 100644 --- a/packages/contractkit/src/identity/claims/types.ts +++ b/packages/contractkit/src/identity/claims/types.ts @@ -1,30 +1,14 @@ -import { either } from 'fp-ts/lib/Either' import * as t from 'io-ts' export const UrlType = t.string export const SignatureType = t.string export const TimestampType = t.number -export const AddressType = t.string - -export const JSONStringType = new t.Type( - 'JSONString', - t.string.is, - (input, context) => - either.chain(t.string.validate(input, context), (stringValue) => { - try { - JSON.parse(stringValue) - return t.success(stringValue) - } catch (error) { - return t.failure(stringValue, context, 'can not be parsed as JSON') - } - }), - String -) export const now = () => Math.round(new Date().getTime() / 1000) export enum ClaimTypes { ATTESTATION_SERVICE_URL = 'ATTESTATION_SERVICE_URL', + ACCOUNT = 'ACCOUNT', DOMAIN = 'DOMAIN', KEYBASE = 'KEYBASE', NAME = 'NAME', @@ -32,4 +16,4 @@ export enum ClaimTypes { TWITTER = 'TWITTER', } -export const VERIFIABLE_CLAIM_TYPES = [ClaimTypes.KEYBASE] +export const VERIFIABLE_CLAIM_TYPES = [ClaimTypes.KEYBASE, ClaimTypes.ACCOUNT] diff --git a/packages/contractkit/src/identity/metadata.test.ts b/packages/contractkit/src/identity/metadata.test.ts index d793cf33c07..1602d5879f1 100644 --- a/packages/contractkit/src/identity/metadata.test.ts +++ b/packages/contractkit/src/identity/metadata.test.ts @@ -1,7 +1,9 @@ +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 } from '../test-utils/ganache.setup' +import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '../test-utils/ganache.setup' +import { createAccountClaim } from './claims/account' import { createNameClaim } from './claims/claim' import { ClaimTypes, IdentityMetadataWrapper } from './metadata' @@ -31,4 +33,45 @@ testWithGanache('Metadata', (web3) => { IdentityMetadataWrapper.fromRawString(serializedMetadata) }).toThrow() }) + + describe('Account claims', () => { + 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() + }) + }) }) diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index c33a5af2c7c..c6e72445566 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -1,3 +1,4 @@ +import { AddressType } from '@celo/utils/lib/io' import { Signer } from '@celo/utils/lib/signatureUtils' import fetch from 'cross-fetch' import { isLeft } from 'fp-ts/lib/Either' @@ -15,7 +16,7 @@ import { SignedClaimType, verifySignature, } from './claims/claim' -import { AddressType, ClaimTypes } from './claims/types' +import { ClaimTypes } from './claims/types' export { ClaimTypes } from './claims/types' const MetaType = t.type({ @@ -112,6 +113,16 @@ export class IdentityMetadataWrapper { } async addClaim(claim: Claim, signer: Signer) { + switch (claim.type) { + case ClaimTypes.ACCOUNT: + if (claim.address === this.data.meta.address) { + throw new Error("Can't claim self") + } + break + + default: + break + } const signedClaim = await this.signClaim(claim, signer) this.data.claims.push(signedClaim) return signedClaim diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index 0cb95318d85..dcaf9fd424b 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,4 +1,4 @@ -import { privateToAddress, toChecksumAddress } from 'ethereumjs-util' +import { privateToAddress, privateToPublic, pubToAddress, toChecksumAddress } from 'ethereumjs-util' export type Address = string @@ -12,4 +12,12 @@ export const privateKeyToAddress = (privateKey: string) => { ) } +export const publicKeyToAddress = (publicKey: string) => { + return '0x' + pubToAddress(Buffer.from(publicKey.slice(2), 'hex')).toString('hex') +} + +export const privateKeyToPublicKey = (privateKey: string) => { + return '0x' + privateToPublic(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') +} + export { toChecksumAddress } from 'ethereumjs-util' diff --git a/packages/utils/src/io.ts b/packages/utils/src/io.ts new file mode 100644 index 00000000000..329838e9e1c --- /dev/null +++ b/packages/utils/src/io.ts @@ -0,0 +1,66 @@ +import { isValidPublic, toChecksumAddress } from 'ethereumjs-util' +import { either } from 'fp-ts/lib/Either' +import * as t from 'io-ts' +import { isE164NumberStrict } from './phoneNumbers' +import { isValidAddress } from './signatureUtils' + +export const JSONStringType = new t.Type( + 'JSONString', + t.string.is, + (input, context) => + either.chain(t.string.validate(input, context), (stringValue) => { + try { + JSON.parse(stringValue) + return t.success(stringValue) + } catch (error) { + return t.failure(stringValue, context, 'can not be parsed as JSON') + } + }), + String +) + +export const E164PhoneNumberType = new t.Type( + '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( + 'Address', + t.string.is, + (input, context) => + either.chain( + t.string.validate(input, context), + (stringValue) => + isValidAddress(stringValue) + ? t.success(toChecksumAddress(stringValue)) + : t.failure(stringValue, context, 'is not a valid address') + ), + String +) + +export const PublicKeyType = new t.Type( + 'Public Key', + t.string.is, + (input, context) => + either.chain( + t.string.validate(input, context), + (stringValue) => + stringValue.startsWith('0x') && + isValidPublic(Buffer.from(stringValue.slice(2), 'hex'), true) + ? t.success(toChecksumAddress(stringValue)) + : t.failure(stringValue, context, 'is not a valid public key') + ), + String +) + +export type Address = t.TypeOf +export type E164Number = t.TypeOf From 5266e8cc60e4f6a4267d5c6aa730431bd772327c Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 11 Nov 2019 17:31:28 -0800 Subject: [PATCH 2/5] Add AddressClaim verification --- .../cli/src/commands/account/get-metadata.ts | 2 +- packages/cli/src/utils/command.ts | 6 +- .../src/identity/claims/account.test.ts | 113 ++++++++++++++++++ .../src/identity/claims/account.ts | 40 ++++++- .../contractkit/src/identity/claims/claim.ts | 22 +++- .../contractkit/src/identity/claims/types.ts | 1 - .../contractkit/src/identity/metadata.test.ts | 45 +------ packages/contractkit/src/identity/metadata.ts | 4 + packages/utils/src/io.ts | 21 ++++ 9 files changed, 198 insertions(+), 56 deletions(-) create mode 100644 packages/contractkit/src/identity/claims/account.test.ts diff --git a/packages/cli/src/commands/account/get-metadata.ts b/packages/cli/src/commands/account/get-metadata.ts index a45d9b66e60..8e407d66e95 100644 --- a/packages/cli/src/commands/account/get-metadata.ts +++ b/packages/cli/src/commands/account/get-metadata.ts @@ -31,7 +31,7 @@ export default class GetMetadata extends BaseCommand { console.info('Metadata contains the following claims: \n') await displayMetadata(metadata) } catch (error) { - console.error('Metadata could not be retrieved from ', metadataURL) + console.error(`Metadata could not be retrieved from ${metadataURL}: ${error.toString()}`) } } } diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index c1ad8113bfa..dee09ae0690 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -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' @@ -28,11 +29,6 @@ const parsePath: ParseFn = (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 = (input) => { if (URL_REGEX.test(input)) { return input diff --git a/packages/contractkit/src/identity/claims/account.test.ts b/packages/contractkit/src/identity/claims/account.test.ts new file mode 100644 index 00000000000..ca61a57e86c --- /dev/null +++ b/packages/contractkit/src/identity/claims/account.test.ts @@ -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() + }) + }) + }) +}) diff --git a/packages/contractkit/src/identity/claims/account.ts b/packages/contractkit/src/identity/claims/account.ts index e238ed2be36..4f15d37d1da 100644 --- a/packages/contractkit/src/identity/claims/account.ts +++ b/packages/contractkit/src/identity/claims/account.ts @@ -1,7 +1,9 @@ -import { AddressType, PublicKeyType } from '@celo/utils/lib/io' +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 @@ -49,3 +51,39 @@ export const createAccountClaim = (address: string, publicKey?: string): Account 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 + +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 +} diff --git a/packages/contractkit/src/identity/claims/claim.ts b/packages/contractkit/src/identity/claims/claim.ts index 3e3b7b00514..c3258d4e4dd 100644 --- a/packages/contractkit/src/identity/claims/claim.ts +++ b/packages/contractkit/src/identity/claims/claim.ts @@ -1,9 +1,9 @@ -import { JSONStringType } from '@celo/utils/lib/io' +import { JSONStringType, UrlType } from '@celo/utils/lib/io' import { hashMessage, parseSignature } from '@celo/utils/lib/signatureUtils' import * as t from 'io-ts' -import { AccountClaim, AccountClaimType } from './account' +import { AccountClaim, AccountClaimType, MetadataURLGetter, verifyAccountClaim } from './account' import { KeybaseClaim, KeybaseClaimType, verifyKeybaseClaim } from './keybase' -import { ClaimTypes, now, SignatureType, TimestampType, UrlType } from './types' +import { ClaimTypes, now, SignatureType, TimestampType } from './types' const AttestationServiceURLClaimType = t.type({ type: t.literal(ClaimTypes.ATTESTATION_SERVICE_URL), @@ -75,10 +75,24 @@ export function verifySignature(serializedPayload: string, signature: string, si } } -export async function verifyClaim(claim: SignedClaim, address: string) { +/** + * Verifies a claim made by an account + * @param claim The claim to verify + * @param address The address that is making the claim + * @param metadataURLGetter A function that can retrieve the metadata URL for a given account address, + * should be Accounts.getMetadataURL() + * @returns If valid, returns undefined. If invalid or unable to verify, returns a string with the error + */ +export async function verifyClaim( + claim: SignedClaim, + address: string, + metadataURLGetter: MetadataURLGetter +) { switch (claim.payload.type) { case ClaimTypes.KEYBASE: return verifyKeybaseClaim(claim.payload, address) + case ClaimTypes.ACCOUNT: + return verifyAccountClaim(claim.payload, address, metadataURLGetter) default: break } diff --git a/packages/contractkit/src/identity/claims/types.ts b/packages/contractkit/src/identity/claims/types.ts index 63a5536a329..9f87682785b 100644 --- a/packages/contractkit/src/identity/claims/types.ts +++ b/packages/contractkit/src/identity/claims/types.ts @@ -1,6 +1,5 @@ import * as t from 'io-ts' -export const UrlType = t.string export const SignatureType = t.string export const TimestampType = t.number diff --git a/packages/contractkit/src/identity/metadata.test.ts b/packages/contractkit/src/identity/metadata.test.ts index 1602d5879f1..d793cf33c07 100644 --- a/packages/contractkit/src/identity/metadata.test.ts +++ b/packages/contractkit/src/identity/metadata.test.ts @@ -1,9 +1,7 @@ -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 { createAccountClaim } from './claims/account' +import { ACCOUNT_ADDRESSES } from '../test-utils/ganache.setup' import { createNameClaim } from './claims/claim' import { ClaimTypes, IdentityMetadataWrapper } from './metadata' @@ -33,45 +31,4 @@ testWithGanache('Metadata', (web3) => { IdentityMetadataWrapper.fromRawString(serializedMetadata) }).toThrow() }) - - describe('Account claims', () => { - 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() - }) - }) }) diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index c6e72445566..47ce0383192 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -132,6 +132,10 @@ export class IdentityMetadataWrapper { return this.data.claims.map((x) => x.payload).find(isOfType(type)) } + filterClaims(type: K): Array> { + return this.data.claims.map((x) => x.payload).filter(isOfType(type)) + } + private signClaim = async (claim: Claim, signer: Signer): Promise => { const messageHash = hashOfClaim(claim) const signature = await signer.sign(messageHash) diff --git a/packages/utils/src/io.ts b/packages/utils/src/io.ts index 329838e9e1c..2d01ee0573d 100644 --- a/packages/utils/src/io.ts +++ b/packages/utils/src/io.ts @@ -4,6 +4,27 @@ import * as t from 'io-ts' import { isE164NumberStrict } from './phoneNumbers' import { isValidAddress } from './signatureUtils' +// from http://urlregex.com/ +export const URL_REGEX = new RegExp( + /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ +) + +export const isValidUrl = (url: string) => URL_REGEX.test(url) + +export const UrlType = new t.Type( + 'Url', + t.string.is, + (input, context) => + either.chain( + t.string.validate(input, context), + (stringValue) => + URL_REGEX.test(stringValue) + ? t.success(stringValue) + : t.failure(stringValue, context, 'is not a valid url') + ), + String +) + export const JSONStringType = new t.Type( 'JSONString', t.string.is, From ef0c16a0aa39f1499414a162e9cca389f83c444d Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 11 Nov 2019 18:06:48 -0800 Subject: [PATCH 3/5] Fix build --- packages/cli/src/utils/identity.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/identity.ts b/packages/cli/src/utils/identity.ts index b949db6c058..3fe62723fd4 100644 --- a/packages/cli/src/utils/identity.ts +++ b/packages/cli/src/utils/identity.ts @@ -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' @@ -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: @@ -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), } From 41419d166fdf0c6df7b4e37cbda7a400be5b827e Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 12 Nov 2019 11:09:31 -0800 Subject: [PATCH 4/5] Fix lint --- packages/cli/src/commands/account/get-metadata.ts | 2 +- packages/cli/src/commands/account/show-metadata.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/account/get-metadata.ts b/packages/cli/src/commands/account/get-metadata.ts index 8e407d66e95..490e10c26ba 100644 --- a/packages/cli/src/commands/account/get-metadata.ts +++ b/packages/cli/src/commands/account/get-metadata.ts @@ -29,7 +29,7 @@ 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}: ${error.toString()}`) } diff --git a/packages/cli/src/commands/account/show-metadata.ts b/packages/cli/src/commands/account/show-metadata.ts index bdb502c4e58..8c72e0845ae 100644 --- a/packages/cli/src/commands/account/show-metadata.ts +++ b/packages/cli/src/commands/account/show-metadata.ts @@ -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) } } From c004f847db4b3b0d6b185345106e3029f8a19cef Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 13 Nov 2019 12:43:40 -0800 Subject: [PATCH 5/5] Docs --- .../docs/command-line-interface/account.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 5967feae68b..cedded67dd3 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -38,6 +38,30 @@ EXAMPLE _See code: [packages/cli/src/commands/account/balance.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/balance.ts)_ +### Claim-account + +Claim another account in a local metadata file + +``` +USAGE + $ celocli account:claim-account FILE + +ARGUMENTS + FILE Path of the metadata file + +OPTIONS + --address=address (required) The address of the account you want to claim + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Addess of the account to set metadata for + + --publicKey=publicKey The public key of the account if you want others to encrypt + messages to you + +EXAMPLE + claim-account ~/metadata.json --address test.com --from 0x0 +``` + +_See code: [packages/cli/src/commands/account/claim-account.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/claim-account.ts)_ + ### Claim-attestation-service-url Claim the URL of the attestation service in a local metadata file