Skip to content

Latest commit

 

History

History
289 lines (234 loc) · 14 KB

protocol.md

File metadata and controls

289 lines (234 loc) · 14 KB

Attestations Protocol

The goals of this attestation protocol are to increase adoption of identity mappings, and encourage network effects of shared cross-application identity attestations. To this end, we have made it possible for anyone to permissionlessly become an issuer of attestations.

Table of contents

Verification

Issuers have the freedom to decide how to verify that the user actually owns their identifier. After verification, issuers register the mapping as an attestation to the on-chain smart contract registry. Attestations are stored under the issuer that registered them. When looking up attestations, we then have to decide which issuers are trusted.

User Flows

Participants Glossary
Participant Description
user User that owns the off-chain identifier and account
issuer Anyone who is verifying identifiers and creating on-chain attestations, this will most likely be a wallet
Oblivious Decentralised Identity Service (ODIS) An API that produces secrets used for obfuscating the identifiers, more details here
FederatedAttestations.sol On-chain registry of attestation mappings between identifiers and addresses
OdisPayments.sol Smart contract used to pay for ODIS quota

Register phone number

The user provides their phone number to the application, who is acting as an issuer. The application verifies that the user has ownership of their phone number. Then the application queries ODIS for the obfuscated identifier of the phone number. Using the obfuscated identifier, the application, as an issuer, registers the on-chain mapping between the identifier and the user's account address.

image

Look up phone number

The wallet queries ODIS for the obfuscated identifier of the phone number. Using the obfuscated identifier, the wallet looks up the on-chain registry to see which account is mapped to that identifier, specifying itself as the trusted issuer.

image

Send money to user on a different wallet

Wallet 1 registers user 1's phone number, as described above.

User 2, who has an account with wallet 2, would like to send money to their friend, user 1. Knowing user 1's phone number, they share it with wallet 2. After getting the obfuscated identifier, wallet 2 uses it to look up the account associated with user 1's phone number, as described above, while specifying that they trust wallet 1 as an issuer. Using the associated account found, wallet 2 completes the transaction of sending money from user 2 to user 1.

%%{init: { "sequence": { "wrap": true } } }%%
sequenceDiagram
    actor user1
    participant wallet1
    actor user2
    participant wallet2
    participant ODIS
    participant SocialConnect as FederatedAttestations.sol

    rect rgb(230,200,240, .3)
    note right of user1: publish attestation
    user1 -->> wallet1: verify user owns phone number
    wallet1 -->> user1:
    wallet1 -->> ODIS: get obfuscated identifier of phone number
    ODIS -->> wallet1:
    wallet1 -->> SocialConnect: publish attestation with obfuscated idnetifier
    end

    rect rgb(230,240,290, .3)
    note right of user1: user2 sends money to user1's phone number
    user1 -->> user2: share phone number with friend
    user2 -->> wallet2: request to send money to user1's phone number
    wallet2 -->> ODIS: get obfuscated identifier of phone number
    ODIS -->> wallet2:
    wallet2 -->> SocialConnect: lookup address mapped to obfuscated identifier, with wallet1 as a trusted issuer
    SocialConnect -->> wallet2:
    wallet2 -->> user1: send money to user1's account
end
Loading

Interacting with the protocol

Issuer Signers

We recommend issuers create separate signer keys for the express purpose of signing attestations. This is to avoid using the issuer account for multiple functions. If a signer key is compromised or lost, the issuer can simply rotate its signer keys and update its attestations accordingly. It is possible to authorize multiple signer keys for this role.

The signer key needs to be authorized by the issuer, under the AttestationSigner role.

import { encodePacked, keccak256 } from 'web3-utils'

const signerRole = keccak256(encodePacked('celo.org/core/attestation'))
await accountsContract.authorizeSigner(signerAddress, signerRole, { from: issuerAddress })
await accountsContract.completeSignerAuthorization(issuerAddress, signerRole, { from: signerAddress })

Attestation mappings are indexed by the address of the issuer. The benefit of using the issuer address instead of the signer address is that the issuer address never changes, while the signer key can be rotated or revoked. If a signer key has been compromised, the issuer can then accordingly remove invalid attestations or upload removed attestations.

Registration

Registering more than one attestation with the same (identifier, issuer, account) is not allowed and will cause the transaction to revert. A maximum of 20 accounts can be associated with each identifier, and a maximum of 20 identifiers can be associated with each account.

Registering identifiers as the issuer

For convenience, the issuer can be its own signer, in which case the attestation is directly registered.

await federatedAttestationsContract
  .registerAttestationAsIssuer(
      obfuscatedIdentifier,
      accountAddress,
      verificationTimestamp
  ).send({ from: issuerAddress })

Registering identifiers with signer

Signers must sign an EIP712 typed data object representing the attestation.

import { ensureLeading0x } from '@celo/base'
import { generateTypedDataHash } from '@celo/utils/src/sign-typed-data-utils'
import { parseSignatureWithoutPrefix } from '@celo/utils/src/signatureUtils'

const getSignatureForAttestation = async (
  identifier: string,
  issuer: string,
  account: string,
  issuedOn: number,
  signer: string,
  chainId: number,
  contractAddress: string
) => {
  const typedData =  {
    types: {
      EIP712Domain: [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256'},
        { name: 'verifyingContract', type: 'address'},
      ],
      OwnershipAttestation: [
          { name: 'identifier', type: 'bytes32' },
          { name: 'issuer', type: 'address'},
          { name: 'account', type: 'address' },
          { name: 'signer', type: 'address' },
          { name: 'issuedOn', type: 'uint64' },
      ],
    },
    primaryType: 'OwnershipAttestation',
    domain: {
      name: 'FederatedAttestations',
      version: '1.0',
      chainId,
      verifyingContract: contractAddress
    },
    message:{
      identifier,
      issuer,
      account,
      signer,
      issuedOn
    }
  }

  const signature = await new Promise<string>((resolve, reject) => {
    web3.currentProvider.send(
      {
        method: 'eth_signTypedData',
        params: [signer, typedData],
      },
      (error, resp) => {
        if (error) {
          reject(error)
        } else {
          resolve(resp.result)
        }
      }
    )
  })

  const messageHash = ensureLeading0x(generateTypedDataHash(typedData).toString('hex'))
  const parsedSignature = parseSignatureWithoutPrefix(messageHash, signature, signer)
  return parsedSignature
}

Anyone who has posession of this signature can register the attestation to the on-chain registry.

const {v,r,s} = getSignatureForAttestation(
    obfuscatedIdentifier,
    issuerAddress,
    accountAddress,
    verificationTimestamp,
    signerAddress,
    chainId,
    federatedAttestationsContract.address
)
await federatedAttestationsContract.registerAttestation(
    obfuscatedIdentifier,
    issuerAddress,
    accountAddress,
    signerAddress,
    verificationTimestamp,
    v,
    r,
    s
).send()

Lookups

Given a list of issuers that you trust, you can look up the accounts they have mapped to an identifier, and the identifiers they have mapped to an account.

const attestations = await federatedAttestationsInstance
    .lookupAttestations(obfuscatedIdentifier, [trustedIssuer1Address, trustedIssuer2Address])
    .call();

// Returns: accounts, signers, issuedOns, and publishedOns represent attestations, where all the elements of the same index constitute one attestation object. countsPerIssuer lists the number of attestations belonging to each issuer passed into the function (ex: countsPerIssuer = [2,3] means that elements 0-1 of the other arrays represent attestations issued by trustedIssuer1 and elements 2-4 of the other arrays represent attestations issued by trustedIssuer2)
// {
//     countsPerIssuer: string[]
//     accounts: Address[]
//     signers: Address[]
//     issuedOns: string[]
//     publishedOns: string[]
// }
console.log(attestations.accounts)

const attestations = await federatedAttestationsInstance
    .lookupIdentifiers(accountAddress, [trustedIssuer1Address, trustedIssuer2Address])
    .call();

// Returns:
// {
//     countsPerIssuer: string[]
//     identifiers: string[]
// }

Smart Contract Addresses

Mainnet

Alfajores