Skip to content

Commit

Permalink
Merge pull request #2 from calebtuttle/dev
Browse files Browse the repository at this point in the history
Clean up and organize IdentityAggregator.sol and tests
  • Loading branch information
nanaknihal authored Apr 3, 2022
2 parents 50ef5dd + f446263 commit d37b632
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 165 deletions.
55 changes: 6 additions & 49 deletions backend/contracts/IdentityAggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
pragma solidity ^0.8.0;

import "hardhat/console.sol";
// import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import "./VerifyJWT.sol";
Expand All @@ -14,29 +11,21 @@ contract IdentityAggregator is Ownable {

mapping(string => address) public contractAddrForKeyword; // e.g., "orcid" => VerifyJWT(orcidContractAddress)

// Allows easier lookup for keywords // NOTE: Don't need this if our only accessor func is getAllAccounts()
mapping(string => string) private keywordForKeyword;
mapping(string => string) private keywordForKeyword; // Allows easier lookup for keywords

string[] private keywords; // e.g., "orcid" // TODO: Better name than 'keywords'??
string[] private keywords; // e.g., "orcid"


event AddSupportForContract(string contractKeyword);
event RemoveSupportForContract(string contractKeyword);


constructor() {

}


/// @notice Add support for a new VerifyJWT contract.
/// @param keyword The string used to denote the source of the JWT (e.g., "twitter").
/// @param contractAddress The address of the JWT contract to be supported.
function addPlatformContract(string memory keyword, address contractAddress) public onlyOwner { // TODO: there must be a better way than onlyOwner

function addVerifyJWTContract(string calldata keyword, address contractAddress) public onlyOwner {
// Require that neither this keyword nor this contract has been added
require(bytes(keywordForKeyword[keyword]).length == 0);
require(contractAddrForKeyword[keyword] == address(0));
require(bytes(keywordForKeyword[keyword]).length == 0, "This keyword is already being used");

keywords.push(keyword);
keywordForKeyword[keyword] = keyword;
Expand All @@ -45,11 +34,10 @@ contract IdentityAggregator is Ownable {
emit AddSupportForContract(keyword);
}

// TODO: Is there a way to store keywords that allows iteration but allows removal without iteration?
/// @notice Remove support for a VerifyJWT contract.
/// @param keyword The string used to lookup the contract.
function removeSupportFor(string memory keyword) public onlyOwner { // TODO: there must be a better way than onlyOwner
require(contractAddrForKeyword[keyword] != address(0), "There is no corresponding contract for this keyword.");
function removeSupportFor(string calldata keyword) public onlyOwner {
require(contractAddrForKeyword[keyword] != address(0), "There is no corresponding contract for this keyword");

for (uint i = 0; i < keywords.length; i++) {
if (keccak256(bytes(keywords[i])) == keccak256(bytes(keyword))) {
Expand Down Expand Up @@ -81,37 +69,6 @@ contract IdentityAggregator is Ownable {
return allCreds;
}

// TODO: Either delete this function, or find a better implementation.
// e.g., keyword1 == "orcid", creds1 == "12345...", keyword2 == "twitter" ---> returns "@somehandle"
function getCredsFromCreds(
string memory keyword1,
bytes calldata creds1, // TODO: Make these parameters fixed-length
string memory keyword2
)
public returns (bytes memory creds2) {

// Require that we have stored the contracts for the specified keywords
require(bytes(keywordForKeyword[keyword1]).length != 0);
require(bytes(keywordForKeyword[keyword2]).length != 0);

// Get user address
address creds1ContractAddr = contractAddrForKeyword[keyword1];
VerifyJWT creds1Contract = VerifyJWT(creds1ContractAddr);
address userAddr = creds1Contract.addressForCreds(creds1);

string memory errorMessage = string(abi.encodePacked("This user has no creds for ", keyword1));
require(userAddr != address(0), errorMessage);

address creds2ContractAddr = contractAddrForKeyword[keyword2];
VerifyJWT creds2Contract = VerifyJWT(creds2ContractAddr);
bytes memory creds2 = creds2Contract.credsForAddress(userAddr);

errorMessage = string(abi.encodePacked("This user has no creds for ", keyword2));
require(creds2.length != 0);

return creds2;
}

function getKeywords() public view returns (string[] memory) {
return keywords;
}
Expand Down
240 changes: 158 additions & 82 deletions backend/test/identityAggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,123 +5,134 @@ const { solidity } = require("ethereum-waffle");
const search64 = require('../../../whoisthis.wtf-frontend/src/searchForPlaintextInBase64.js');
// import { fixedBufferXOR as xor, sandwichIDWithBreadFromContract, padBase64, hexToString, searchForPlainTextInBase64 } from 'wtfprotocol-helpers';
const { hexToString } = require('wtfprotocol-helpers');
const {
vmExceptionStr,
orcidKid, orcidBotomBread, orcidTopBread,
googleKid, googleBottomBread, googleTopBread,
deployVerifyJWTContract,
deployIdAggregator,
sha256FromString,
sandwichIDWithBreadFromContract,
jwksKeyToPubkey,
} = require('./utils/utils');


// chai.use(solidity);


//-------------------- Constants & Helpers --------------------

const orcidKid = '7hdmdswarosg3gjujo8agwtazgkp1ojs'
const orcidBotomBread = '0x222c22737562223a22'
const orcidTopBread = '0x222c22617574685f74696d65223a'

const deployVerifyJWTContract = async (...args) => {
let VJWT = await ethers.getContractFactory('VerifyJWT')
return await upgrades.deployProxy(VJWT, args, {
initializer: 'initialize',
});
}

const sha256FromString = x => ethers.utils.sha256(ethers.utils.toUtf8Bytes(x))

// Make sure it does bottomBread + id + topBread and does not allow any other text in between. If Google changes their JWT format so that the sandwich now contains other fields between bottomBread and topBread, this should fail until the contract is updated.
const sandwichIDWithBreadFromContract = async (id, contract) => {
let sandwich = (await contract.bottomBread()) + Buffer.from(id).toString('hex') + (await contract.topBread());
sandwich = sandwich.replaceAll('0x', '');
return sandwich
}

// Converts JWKS RSAkey to e and n:
const jwksKeyToPubkey = (jwks) => {
let parsed = JSON.parse(jwks)
return [
ethers.BigNumber.from(Buffer.from(parsed['e'], 'base64url')),
ethers.BigNumber.from(Buffer.from(parsed['n'], 'base64url'))
]
}

//-------------------------------------------------



describe.only("IdentityAggregator", function () {

// TODO: What's the best way to initialize contract variables before all tests, instead of initializing within every test?
describe("IdentityAggregator", function () {

describe("keywords", function () {

before(async function () {
const IdentityAggregator = await ethers.getContractFactory("IdentityAggregator");
this.idAggregator = await IdentityAggregator.deploy();
await this.idAggregator.deployed();
this.idAggregator = await deployIdAggregator();
});

it("Should be empty when contract is deployed", async function () {
expect(await this.idAggregator.getKeywords()).to.be.an('array').that.is.empty;
});

it("Should be updated when support for a new contract is added", async function () {
//--------------------------- Set up context ---------------------------
// get mock vjwt contract address
const [owner] = await ethers.getSigners()
const transactionCount = await owner.getTransactionCount()
const verifyJWTAddress = getContractAddress({
from: owner.address,
nonce: transactionCount
})
it("Should include 'orcid' after support for orcid contract is added", async function () {
const keyword = 'orcid';
await this.idAggregator.addPlatformContract(keyword, verifyJWTAddress);
await this.idAggregator.addVerifyJWTContract(keyword, "0x100DEF1234567890ABCDEF1234567890ABCDE001");
expect(await this.idAggregator.getKeywords()).to.include(keyword);
});

it("Should include 'google' after support for google contract is added", async function () {
const keyword = 'google';
await this.idAggregator.addVerifyJWTContract(keyword, "0x200DEF1234567890ABCDEF1234567890ABCDE002");
expect(await this.idAggregator.getKeywords()).to.include(keyword);
});

//--------------------------- Run test ---------------------------
it("Should include both 'orcid' and 'google'", async function () {
const keywords = ['orcid', 'google'];
expect(await this.idAggregator.getKeywords()).to.have.members(keywords);
});

// Test deletion
it("Should not include 'orcid' after support for orcid contract is removed", async function () {
const keyword = 'orcid';
await this.idAggregator.removeSupportFor(keyword);
expect(await this.idAggregator.getKeywords()).to.not.include(keyword);
});

it("Should not include 'google' after support for google contract is removed", async function () {
const keyword = 'google';
await this.idAggregator.removeSupportFor(keyword);
expect(await this.idAggregator.getKeywords()).to.not.include(keyword);
});

expect(await this.idAggregator.getKeywords()).to.have.members([keyword]);
// Test addition after deletion
it("Should include 'twitter' after support for twitter contract is added", async function () {
const keyword = 'twitter';
await this.idAggregator.addVerifyJWTContract(keyword, "0x300DEF1234567890ABCDEF1234567890ABCDE003");
expect(await this.idAggregator.getKeywords()).to.include(keyword);
});

});

describe("contractAddrForKeyword", function () {
it("Should be updated when support for a new contract is added", async function () {
//--------------------------- Set up context ---------------------------

const IdentityAggregator = await ethers.getContractFactory("IdentityAggregator");
const idAggregator = await IdentityAggregator.deploy();
await idAggregator.deployed();
const idAggregator = await deployIdAggregator();
const [owner] = await ethers.getSigners();

// get contract address for VerifyJWT
const [owner] = await ethers.getSigners()
const transactionCount = await owner.getTransactionCount()
const futureAddress = getContractAddress({
const transactionCount = await owner.getTransactionCount();
const vjwtAddress = getContractAddress({
from: owner.address,
nonce: transactionCount
})
});

vjwt = await deployVerifyJWTContract(11, 59, orcidKid, orcidBotomBread, orcidTopBread);

const keyword = "orcid";
await idAggregator.addPlatformContract(keyword, futureAddress);

//--------------------------- Run test ---------------------------
await idAggregator.addVerifyJWTContract(keyword, vjwtAddress);

const verifyJWTAddress = await idAggregator.callStatic.contractAddrForKeyword(keyword);
expect(verifyJWTAddress).to.equal(futureAddress);
expect(verifyJWTAddress).to.equal(vjwtAddress);
});
});

describe("addVerifyJWTContract", function () {
before(async function () {
this.idAggregator = await deployIdAggregator();
});

it("Should revert when attempting to use the keyword of an already supported contract", async function () {
const keyword = 'twitter';
addr = '0x100DEF1234567890ABCDEF1234567890ABCDE001';
await this.idAggregator.addVerifyJWTContract(keyword, addr);
addr = '0x200DEF1234567890ABCDEF1234567890ABCDE002';
const funcStr = 'addVerifyJWTContract(string,address)';
await expect(this.idAggregator[funcStr](keyword, addr)).to.be.revertedWith(vmExceptionStr + "'This keyword is already being used'");
});
});

describe("removeSupportFor", async function () {
// Try to remove an unsupported contract
it("Should revert ", async function () {
const idAggregator = await deployIdAggregator()
keyword = 'twitter';
await idAggregator.addVerifyJWTContract(keyword, "0x100DEF1234567890ABCDEF1234567890ABCDE001");
expect(await idAggregator.getKeywords()).to.include(keyword);

keyword = "definitelynotthekeyword";
const funcStr = 'removeSupportFor(string)';
await expect(idAggregator[funcStr](keyword)).to.be.revertedWith(vmExceptionStr + "'There is no corresponding contract for this keyword'");
});
});

describe("getAllAccounts", function () {
it("Should return array of supported creds", async function() {
//--------------------------- Set up context ---------------------------
before(async function () {
this.idAggregator = await deployIdAggregator();
});

const IdentityAggregator = await ethers.getContractFactory("IdentityAggregator");
const idAggregator = await IdentityAggregator.deploy();
await idAggregator.deployed();
it("Should return array of supported creds, the first of which is the correct orcid", async function() {
//--------------------------- Set up context ---------------------------
const [owner] = await ethers.getSigners();

// get contract address for VerifyJWT
const [owner] = await ethers.getSigners()
const transactionCount = await owner.getTransactionCount()
const transactionCount = await owner.getTransactionCount();
const vjwtAddress = getContractAddress({
from: owner.address,
nonce: transactionCount
})
});

const [eOrcid, nOrcid] = jwksKeyToPubkey('{"kty":"RSA","e":"AQAB","use":"sig","kid":"production-orcid-org-7hdmdswarosg3gjujo8agwtazgkp1ojs","n":"jxTIntA7YvdfnYkLSN4wk__E2zf_wbb0SV_HLHFvh6a9ENVRD1_rHK0EijlBzikb-1rgDQihJETcgBLsMoZVQqGj8fDUUuxnVHsuGav_bf41PA7E_58HXKPrB2C0cON41f7K3o9TStKpVJOSXBrRWURmNQ64qnSSryn1nCxMzXpaw7VUo409ohybbvN6ngxVy4QR2NCC7Fr0QVdtapxD7zdlwx6lEwGemuqs_oG5oDtrRuRgeOHmRps2R6gG5oc-JqVMrVRv6F9h4ja3UgxCDBQjOVT1BFPWmMHnHCsVYLqbbXkZUfvP2sO1dJiYd_zrQhi-FtNth9qrLLv3gkgtwQ"}');
const vjwt = await deployVerifyJWTContract(eOrcid, nOrcid, orcidKid, orcidBotomBread, orcidTopBread);
Expand All @@ -143,17 +154,82 @@ describe.only("IdentityAggregator", function () {
await vjwt.verifyMe(ethers.BigNumber.from(signature), message, payloadIdx, startIdx, endIdx, '0x'+sandwich);

const keyword = "orcid";
await idAggregator.addPlatformContract(keyword, vjwtAddress);
await this.idAggregator.addVerifyJWTContract(keyword, vjwtAddress);

const allAccounts = await idAggregator.callStatic.getAllAccounts(owner.address);
const allAccounts = await this.idAggregator.callStatic.getAllAccounts(owner.address);
const creds = hexToString(allAccounts[0]);
console.log(creds)

//--------------------------- Run test ---------------------------

expect(creds).to.equal(correctID);
});

it("Should return array of supported creds, the second of which is the correct gmail", async function() {
//--------------------------- Set up context ---------------------------
const [owner] = await ethers.getSigners();

// get contract address for VerifyJWT
const transactionCount = await owner.getTransactionCount();
const vjwtAddress = getContractAddress({
from: owner.address,
nonce: transactionCount
});

const [eGoogle, nGoogle] = jwksKeyToPubkey('{"alg":"RS256","use":"sig","n":"pFcwF2goSItvLhMJR1u0iPu2HO3wy6SSppmzgISWkRItInbuf2lWdQBt3x45mZsS9eXn6t9lUYnnduO5MrVtA1KoeZhHfSJZysIPh9S7vbU7_mV9SaHSyFPOOZr5jpU2LhNJehWqek7MTJ7FfUp1sgxtnUu-ffrFvMpodUW5eiNMcRmdIrd1O1--WlMpQ8sNk-KVTb8M8KPD0SYz-8kJLAwInUKK0EmxXjnYPfvB9RO8_GLAU7jodmTcVMD25PeA1NRvYqwzpJUYfhAUhPtE_rZX-wxn0udWddDQqihU7T_pTxiZe9R0rI0iAg--pV0f1dYnNfrZaB7veQq_XFfvKw","e":"AQAB","kty":"RSA","kid":"729189450d49028570425266f03e737f45af2932"}')
const vjwt = await deployVerifyJWTContract(eGoogle, nGoogle, googleKid, googleBottomBread, googleTopBread);

// Set up context to call commitJWTProof() and verifyMe()
const idToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjcyOTE4OTQ1MGQ0OTAyODU3MDQyNTI2NmYwM2U3MzdmNDVhZjI5MzIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjU0OTg0NTAwNTY2LTNxaXM1NG1vZmVnNWVkb2dhdWpycDhyYjdwYnA5cXRuLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMjU0OTg0NTAwNTY2LTNxaXM1NG1vZmVnNWVkb2dhdWpycDhyYjdwYnA5cXRuLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTAwNzg3ODQ0NDczMTcyMjk4NTQzIiwiZW1haWwiOiJuYW5ha25paGFsQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiMDREZXRTaGNSYUE4OWxlcEQzdWRnUSIsIm5hbWUiOiJOYW5hayBOaWhhbCBLaGFsc2EiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUFUWEFKdzRnMVA3UFZUS2ZWUU1ldFdtUVgxQlNvWjlPWTRVUWtLcjdsTDQ9czk2LWMiLCJnaXZlbl9uYW1lIjoiTmFuYWsgTmloYWwiLCJmYW1pbHlfbmFtZSI6IktoYWxzYSIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjQ3NjYzNDk4LCJleHAiOjE2NDc2NjcwOTgsImp0aSI6IjE4ZmRmMGQ2M2VhYjI4YjRlYmY0NmFiMDMzZTM5OTU3NmE5MTJlZGUifQ.YqmOub03zNmloAcFvZE0E-4Gt2Y5fr_9XQLUYqXQ24X_GJaJh0HSQXouJeSXjnk8PT6E1FnPd89QAgwDvE_qxAoOvW7VKDycVapOeDtKdTQ-QpAn-ExE0Pvqgx1iaGRZFDS4DWESX1ZsQIBAB_MHK_ZFdAnOjeFzImuMkB1PZLY99przSaM8AEyvWn8wfEgdmkdoJERBXF7xJI2dfA9mTRjlQvhSC4K060bTJbUYug4sQLrvo53CsDjvXRnodnCB81EVWZUbf5B9dG__kebI3AjedKUcPb2wofpX_B7uAyVlD7Au3APEbZP7Asle0Bi76hDNGPQbLvR_nGWLoySfCQ';
const correctID = '[email protected]';
const [headerRaw, payloadRaw, signatureRaw] = idToken.split('.');
const signature = Buffer.from(signatureRaw, 'base64url');
const message = headerRaw + '.' + payloadRaw;
const payloadIdx = Buffer.from(headerRaw).length + 1; //Buffer.from('.').length == 1
const sandwich = await sandwichIDWithBreadFromContract(correctID, vjwt);
const [startIdx, endIdx] = search64.searchForPlainTextInBase64(Buffer.from(sandwich, 'hex').toString(), payloadRaw);
const hashedMessage = sha256FromString(message);
const proof = ethers.utils.sha256(await vjwt.XOR(hashedMessage, owner.address));

await vjwt.commitJWTProof(proof);
await ethers.provider.send('evm_mine');
await vjwt.verifyMe(ethers.BigNumber.from(signature), message, payloadIdx, startIdx, endIdx, '0x'+sandwich);

const keyword = "google";
await this.idAggregator.addVerifyJWTContract(keyword, vjwtAddress);

const allAccounts = await this.idAggregator.callStatic.getAllAccounts(owner.address);
const creds = hexToString(allAccounts[1]);

//--------------------------- Run test ---------------------------

expect(creds).to.equal(correctID);
});

it("Should return the correct array of supported creds", async function() {
const [owner] = await ethers.getSigners();
const allAccounts = await this.idAggregator.callStatic.getAllAccounts(owner.address);
const credsArray = allAccounts.map(creds => hexToString(creds));

expect(credsArray).to.include.members(['[email protected]', '0000-0002-2308-9517']);
});

it("Should return an array that includes gmail but not orcid", async function() {
await this.idAggregator.removeSupportFor('orcid');
const [owner] = await ethers.getSigners();
const allAccounts = await this.idAggregator.callStatic.getAllAccounts(owner.address);
const credsArray = allAccounts.map(creds => hexToString(creds));

expect(credsArray).to.not.include.members(['0000-0002-2308-9517']);
});

it("Should return an array that includes neither orcid nor gmail", async function() {
await this.idAggregator.removeSupportFor('google');
const [owner] = await ethers.getSigners();
const allAccounts = await this.idAggregator.callStatic.getAllAccounts(owner.address);
const credsArray = allAccounts.map(creds => hexToString(creds));

expect(credsArray).to.not.include.members(['[email protected]', '0000-0002-2308-9517']);
});
});

});

Loading

0 comments on commit d37b632

Please sign in to comment.