From ebe3c289b60962086e9efea901bce4f0fe126fbb Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 9 Aug 2023 14:23:35 +0200 Subject: [PATCH 01/19] Endpoint to create web3Id proofs --- examples/add-example-Web3Id/index.html | 23 +++ examples/two-step-transfer/index.html | 23 ++- .../src/wallet-api-types.ts | 7 + packages/browser-wallet-api/src/wallet-api.ts | 15 ++ .../browser-wallet-message-hub/src/message.ts | 3 + .../browser-wallet/src/background/id-proof.ts | 49 ++++- .../browser-wallet/src/background/index.ts | 14 +- .../src/popup/constants/routes.ts | 3 + .../DisplayStatement/DisplayStatement.tsx | 12 +- .../IdProofRequest/DisplayStatement/utils.ts | 27 ++- .../pages/IdProofRequest/IdProofRequest.tsx | 4 +- .../Web3ProofRequest/AccountStatement.tsx | 186 ++++++++++++++++++ .../Web3ProofRequest/CredentialSelector.tsx | 34 ++++ .../Web3ProofRequest/DisplayStatement.tsx | 22 +++ .../VerifiableCredentialStatement.tsx | 176 +++++++++++++++++ .../Web3ProofRequest/Web3ProofRequest.scss | 19 ++ .../Web3ProofRequest/Web3ProofRequest.tsx | 183 +++++++++++++++++ .../src/popup/pages/Web3ProofRequest/index.ts | 1 + .../src/popup/pages/Web3ProofRequest/utils.ts | 164 +++++++++++++++ .../browser-wallet/src/popup/shell/Routes.tsx | 19 ++ .../src/popup/styles/_components.scss | 1 + .../src/shared/utils/proof-helpers.ts | 19 ++ .../browser-wallet/src/shared/utils/types.ts | 6 +- .../test/Web3ProofRequest.test.ts | 27 +++ 24 files changed, 1011 insertions(+), 26 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts create mode 100644 packages/browser-wallet/src/shared/utils/proof-helpers.ts create mode 100644 packages/browser-wallet/test/Web3ProofRequest.test.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index 0ff7c91e3..654af9e36 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -20,6 +20,27 @@ provider.on('accountDisconnected', (accountAddress) => (currentAccountAddress = undefined)); provider.on('accountChanged', (accountAddress) => (currentAccountAddress = accountAddress)); provider.on('chainChanged', (chain) => alert(chain)); + document.getElementById('web3Proof').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => + b + .revealAttribute('graduationDate') + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + ) + .getStatements(); + // Should be not be hardcoded + const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; + provider + .requestVerifiablePresentation(challenge, statement) + .then((proof) => { + console.log(proof); + alert('Proof received! (check the console)'); + }) + .catch((error) => { + console.log(error); + alert(error); + }); + }); document.getElementById('addWeb3Id').addEventListener('click', () => { const values = { degreeType: degreeType.value, @@ -108,5 +129,7 @@

Attribute values:



+
+ diff --git a/examples/two-step-transfer/index.html b/examples/two-step-transfer/index.html index 1363693bc..51cf100e8 100644 --- a/examples/two-step-transfer/index.html +++ b/examples/two-step-transfer/index.html @@ -214,6 +214,27 @@ }) .catch(alert); }); + document.getElementById('web3Proof').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute(0).addRange(3, '08000101', '20000101') + ) + .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => + b.revealAttribute(0).addMembership(2, ['2010-06-01T00:00:00Z']) + ) + .getStatements(); + const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; + provider + .requestVerifiablePresentation(challenge, statement) + .then((proof) => { + console.log(proof); + alert('Proof received! (check the console)'); + }) + .catch((error) => { + console.log(error); + alert(error); + }); + }); document.getElementById('addWCCD').addEventListener('click', () => { provider .addCIS2Tokens(currentAccountAddress, [''], 2059n, 0n) @@ -221,7 +242,6 @@ .catch(alert); }); } - setupPage(); @@ -244,6 +264,7 @@

Account address:

+
Message: diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index eeb811abe..acc51e524 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -9,6 +9,8 @@ import type { IdStatement, IdProofOutput, ConcordiumGRPCClient, + CredentialStatements, + VerifiablePresentation, CredentialSubject, HexString, } from '@concordium/web-sdk'; @@ -218,6 +220,11 @@ interface MainWalletApi { credentialHolderIdDID: string ) => Promise<{ randomness: Record; proof: CredentialProof }> ): Promise; + + /** + * @TODO write this + fix return type + */ + requestVerifiablePresentation(challenge: string, statements: CredentialStatements): Promise; } export type WalletApi = MainWalletApi & EventListeners; diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index a676e9020..8dfe369d6 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -9,6 +9,7 @@ import { AccountTransactionSignature, AccountTransactionType, DeployModulePayload, + HexString, InitContractPayload, SchemaVersion, UpdateContractPayload, @@ -28,6 +29,7 @@ import { import EventEmitter from 'events'; import type { JsonRpcRequest } from '@concordium/common-sdk/lib/providers/provider'; import { IdProofOutput, IdStatement } from '@concordium/common-sdk/lib/idProofTypes'; +import { CredentialStatements, VerifiablePresentation } from '@concordium/common-sdk/lib/web3ProofTypes'; import { ConcordiumGRPCClient } from '@concordium/common-sdk/lib/GRPCClient'; import JSONBig from 'json-bigint'; import { stringify } from './util'; @@ -285,6 +287,19 @@ class WalletApi extends EventEmitter implements IWalletApi { return credentialHolderIdDID; } + + public async requestVerifiablePresentation(challenge: HexString, statements: CredentialStatements) { + const res = await this.messageHandler.sendMessage>(MessageType.Web3Proof, { + statements, + challenge, + }); + + if (!res.success) { + throw new Error(res.message); + } + + return VerifiablePresentation.fromString(res.result); + } } export const walletApi = new WalletApi(); diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 4204a9e7b..5e1c65a21 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -16,6 +16,7 @@ export enum MessageType { GrpcRequest = 'M_GrpcRequest', AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', + Web3Proof = 'M_Web3Proof', ConnectAccounts = 'M_ConnectAccounts', AddWeb3IdCredential = 'M_AddWeb3IdCredential', AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish', @@ -40,6 +41,8 @@ export enum InternalMessageType { AddTokens = 'I_AddTokens', IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', + Web3Proof = 'I_Web3Proof', + CreateWeb3Proof = 'I_CreateWeb3Proof', ConnectAccounts = 'I_ConnectAccounts', AddWeb3IdCredential = 'I_AddWeb3IdCredential', } diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index 1e925c6a4..1fbb1071d 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,10 +1,17 @@ -import { getIdProof, IdProofInput, verifyIdstatement } from '@concordium/web-sdk'; -import { BackgroundResponseStatus, IdProofBackgroundResponse } from '@shared/utils/types'; +import { + getIdProof, + getVerifiablePresentation, + IdProofInput, + IdProofOutput, + verifyIdstatement, + Web3IdProofInput, +} from '@concordium/web-sdk'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { isHex } from 'wallet-common-helpers'; import { RunCondition } from './window-management'; -async function createIdProof(input: IdProofInput): Promise { +async function createIdProof(input: IdProofInput): Promise> { const proof = getIdProof(input); return { status: BackgroundResponseStatus.Success, @@ -39,3 +46,39 @@ export const runIfValidProof: RunCondition> = as }; } }; + +async function createWeb3Proof(input: Web3IdProofInput): Promise> { + const proof = getVerifiablePresentation(input); + return { + status: BackgroundResponseStatus.Success, + proof: proof.toString(), + }; +} + +export const createWeb3ProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { + createWeb3Proof(msg.payload) + .then(respond) + .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); + return true; +}; + +/** + * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. + */ +export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { + if (!isHex(msg.payload.challenge)) { + return { + run: false, + response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, + }; + } + try { + // TODO web3: Check that the request is well-formed + return { run: true }; + } catch (e) { + return { + run: false, + response: { success: false, message: `Id statement is not well-formed: ${(e as Error).message}` }, + }; + } +}; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index fe07de8e5..b38289ea1 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -26,7 +26,7 @@ import { Buffer } from 'buffer/'; import JSONBig from 'json-bigint'; import { startMonitoringPendingStatus } from './confirmation'; import { sendCredentialHandler } from './credential-deployment'; -import { createIdProofHandler, runIfValidProof } from './id-proof'; +import { createIdProofHandler, createWeb3ProofHandler, runIfValidProof, runIfValidWeb3IdProof } from './id-proof'; import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; import bgMessageHandler from './message-handler'; import { setupRecoveryHandler, startRecovery } from './recovery'; @@ -40,7 +40,6 @@ import { testPopupOpen, } from './window-management'; import { runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler } from './web3Id'; - const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -253,6 +252,8 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); +bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3Proof), createWeb3ProofHandler); + const NOT_WHITELISTED = 'Site is not whitelisted'; /** @@ -588,3 +589,12 @@ forwardToPopup( undefined, withPromptEnd ); + +forwardToPopup( + MessageType.Web3Proof, + InternalMessageType.Web3Proof, + runConditionComposer(runIfAllowlisted, runIfValidWeb3IdProof, withPromptStart()), + appendUrlToPayload, + undefined, + withPromptEnd +); diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index 1ade89e47..e88ee4e35 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -69,6 +69,9 @@ export const relativeRoutes = { idProof: { path: 'id-proof', }, + web3IdProof: { + path: 'web3Id-proof', + }, }, setup: { path: '/setup', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index ea3bb7c45..53c164a83 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -28,7 +28,7 @@ import { isoToCountryName, } from './utils'; -type StatementLine = { +export type StatementLine = { attribute: string; value: string; isRequirementMet: boolean; @@ -36,7 +36,7 @@ type StatementLine = { type StatementLineProps = StatementLine; -function DisplayStatementLine({ attribute, value, isRequirementMet }: StatementLineProps) { +export function DisplayStatementLine({ attribute, value, isRequirementMet }: StatementLineProps) { return (
  • {attribute}:
    @@ -181,10 +181,6 @@ type BaseProps = ClassName & { onInvalid(): void; }; -type DisplayRevealStatementProps = BaseProps & { - statements: RevealStatement[]; -}; - export function DisplayRevealStatement({ dappName, statements, @@ -225,6 +221,10 @@ export function DisplayRevealStatement({ return ; } +type DisplayRevealStatementProps = BaseProps & { + statements: RevealStatement[]; +}; + type DisplaySecretStatementProps = BaseProps & { statement: SecretStatement; }; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts index 6c2dfa132..00855d8a8 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts @@ -11,7 +11,7 @@ import { getPastDate, MAX_DATE, } from '@concordium/web-sdk'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import countryTranslations from 'i18n-iso-countries'; import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; @@ -206,14 +206,12 @@ export function useStatementValue(statement: SecretStatement): string { export const isoToCountryName = (locale: string) => (isoCode: string) => countryTranslations.getName(isoCode, locale); -export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { - const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); +export function getStatementDescription( + statement: SecretStatement, + t: TFunction<'idProofRequest', 'displayStatement.descriptions'>, + resolvedLanguage: string +) { const displayAttribute = useDisplayAttributeValue(); - const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; - - if (!hasAttribute) { - return t('missingAttribute', { identityName: identity.name }); - } if (statement.type === StatementTypes.AttributeInRange) { switch (statement.attributeTag) { @@ -228,7 +226,7 @@ export function useStatementDescription(statement: SecretStatement, identity: Co } } else { const text = getTextForSet(t, statement); - const getCountryName = isoToCountryName(i18n.resolvedLanguage); + const getCountryName = isoToCountryName(resolvedLanguage); switch (statement.attributeTag) { case 'countryOfResidence': @@ -255,6 +253,17 @@ export function useStatementDescription(statement: SecretStatement, identity: Co return undefined; } +export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; + + if (!hasAttribute) { + return t('missingAttribute', { identityName: identity.name }); + } + + return getStatementDescription(statement, t, i18n.resolvedLanguage); +} + export function canProveStatement(statement: SecretStatement, identity: ConfirmedIdentity) { const attribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag]; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx index aac0b7cec..d2b1228ff 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx @@ -12,7 +12,7 @@ import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings' import { addToastAtom } from '@popup/state'; import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; import { getGlobal, getNet } from '@shared/utils/network-helpers'; -import { BackgroundResponseStatus, IdProofBackgroundResponse } from '@shared/utils/types'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import PendingArrows from '@assets/svg/pending-arrows.svg'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; @@ -71,7 +71,7 @@ export default function IdProofRequest({ onReject, onSubmit }: Props) { const global = await getGlobal(client); - const idProofResult: IdProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( + const idProofResult: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( InternalMessageType.CreateIdProof, { identityIndex: credential.identityIndex, diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx new file mode 100644 index 000000000..a06898165 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -0,0 +1,186 @@ +import { + AccountCredentialStatement, + createAccountDID, + RevealStatementV2, + StatementTypes, + AttributeKey, + AttributeList, +} from '@concordium/web-sdk'; +import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { + useConfirmedIdentities, + useDisplayAttributeValue, + useGetAttributeName, +} from '@popup/shared/utils/identity-helpers'; +import { credentialsAtom } from '@popup/store/account'; +import { WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; +import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import CredentialSelector from './CredentialSelector'; +import { DisplayCredentialStatementProps, getViableAccountCredentialsForStatement, SecretStatementV2 } from './utils'; +import { + getStatementDescription, + isoToCountryName, + SecretStatement, + useStatementHeader, + useStatementName, + useStatementValue, +} from '../IdProofRequest/DisplayStatement/utils'; + +type DisplaySecretStatementV2Props = ClassName & { + identity?: ConfirmedIdentity; + dappName: string; + statement: SecretStatementV2; +}; + +export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + + if (!identity) { + // TODO Should we write something here? + return ''; + } + const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; + if (!hasAttribute) { + return t('missingAttribute', { identityName: identity.name }); + } + + return getStatementDescription(statement, t, i18n.resolvedLanguage); +} + +export function DisplaySecretStatementV2({ dappName, statement, identity, className }: DisplaySecretStatementV2Props) { + const v1Statement: SecretStatement = statement as SecretStatement; + const header = useStatementHeader(v1Statement); + const value = useStatementValue(v1Statement); + const description = useStatementDescription(v1Statement, identity); + const attribute = useStatementName(v1Statement); + + const lines: StatementLine[] = [ + { + attribute, + value, + isRequirementMet: identity !== undefined, + }, + ]; + + return ( + + ); +} + +type DisplayRevealStatementV2Props = ClassName & { + identity?: ConfirmedIdentity; + dappName: string; + statements: RevealStatementV2[]; +}; + +export function DisplayRevealStatementV2({ dappName, statements, identity, className }: DisplayRevealStatementV2Props) { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const getAttributeName = useGetAttributeName(); + const displayAttribute = useDisplayAttributeValue(); + const header = t('headers.reveal'); + const attributes = identity + ? identity.idObject.value.attributeList.chosenAttributes + : ({} as AttributeList['chosenAttributes']); + + const lines: StatementLine[] = statements.map((s) => { + const stringTag = s.attributeTag as AttributeKey; + const raw = attributes[stringTag]; + let value = displayAttribute(stringTag, raw ?? ''); + + if (value && ['countryOfResidence', 'nationality', 'idDocIssuer'].includes(stringTag)) { + value = isoToCountryName(i18n.resolvedLanguage)(value); + } + + return { + attribute: getAttributeName(stringTag), + value: value ?? 'Unavailable', + isRequirementMet: raw !== undefined, + }; + }); + + return ; +} + +export default function AccountStatement({ + credentialStatement, + dappName, + setChosenId, + net, +}: DisplayCredentialStatementProps) { + const reveals = credentialStatement.statement.filter( + (s) => s.type === StatementTypes.RevealAttribute + ) as RevealStatementV2[]; + const secrets = credentialStatement.statement.filter( + (s) => s.type !== StatementTypes.RevealAttribute + ) as SecretStatementV2[]; + + const identities = useConfirmedIdentities(); + const credentials = useAtomValue(credentialsAtom); + + const validCredentials = useMemo(() => { + if (identities.loading) { + return []; + } + return getViableAccountCredentialsForStatement(credentialStatement, identities.value, credentials); + }, [credentialStatement.idQualifier.issuers]); + + const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + + const onChange = useCallback((credential: WalletCredential) => { + setChosenCredential(credential); + setChosenId(createAccountDID(net, credential.credId)); + }, []); + + // Initially set chosenId + useEffect(() => { + if (chosenCredential) { + setChosenId(createAccountDID(net, chosenCredential.credId)); + } + }, []); + + const identity = useMemo(() => { + if (!chosenCredential) { + return undefined; + } + return identities.value.find((id) => isIdentityOfCredential(id)(chosenCredential)); + }, [chosenCredential?.credId]); + + return ( +
    + displaySplitAddress(option.address)} + onChange={onChange} + /> + {reveals.length !== 0 && ( + + )} + {secrets.map((s, i) => ( + + ))} +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx new file mode 100644 index 000000000..c053dca56 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; + +interface Props { + options: T[]; + onChange: (x: T) => void; + displayOption: (x: T) => string; +} + +/** + * Component to select a credential, either account credential or web3Id credential. + */ +export default function CredentialSelector({ options, onChange, displayOption }: Props) { + const [chosenIndex, setChosenIndex] = useState(0); + + if (options.length === 0) { + // TODO Translate + return
    No candidate available
    ; + } + + return ( + + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx new file mode 100644 index 000000000..3a6581802 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx @@ -0,0 +1,22 @@ +import { + CredentialStatement, + isAccountCredentialStatement, + isVerifiableCredentialStatement, +} from '@concordium/web-sdk'; +import React from 'react'; +import { DisplayCredentialStatementProps } from './utils'; +import DisplayWeb3Statement from './VerifiableCredentialStatement'; +import DisplayAccountStatement from './AccountStatement'; + +export function DisplayCredentialStatement({ + credentialStatement, + ...params +}: DisplayCredentialStatementProps) { + if (isAccountCredentialStatement(credentialStatement)) { + return ; + } + if (isVerifiableCredentialStatement(credentialStatement)) { + return ; + } + return null; +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx new file mode 100644 index 000000000..4d6939987 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -0,0 +1,176 @@ +import { + AtomicStatementV2, + RevealStatementV2, + StatementTypes, + VerifiableCredentialStatement, +} from '@concordium/web-sdk'; +import { + storedVerifiableCredentialsAtom, + storedVerifiableCredentialSchemasAtom, +} from '@popup/store/verifiable-credential'; +import { VerifiableCredential, CredentialSubject } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import CredentialSelector from './CredentialSelector'; +import { + createWeb3IdDIDFromCredential, + DisplayCredentialStatementProps, + getVerifiableCredentialPublicKeyfromSubjectDID, + getViableWeb3IdCredentialsForStatement, + SecretStatementV2, +} from './utils'; + +type DisplayWeb3StatementProps = ClassName & { + statements: Statement; + dappName: string; + credential: CredentialSubject; +}; + +type AttributeInfo = { + name: string; + value: string | bigint; +}; + +function extractAttributesFromCredentialSubjectForSingleStatement( + { attributeTag }: AtomicStatementV2, + credentialSubject: CredentialSubject +): AttributeInfo { + return { name: attributeTag, value: credentialSubject[attributeTag] }; +} + +function extractAttributesFromCredentialSubject( + statements: AtomicStatementV2[], + credentialSubject: CredentialSubject +): Record { + return statements.reduce>((acc, statement) => { + acc[statement.attributeTag] = extractAttributesFromCredentialSubjectForSingleStatement( + statement, + credentialSubject + ); + return acc; + }, {}); +} + +export function DisplayWeb3RevealStatement({ + statements, + dappName, + credential, + className, +}: DisplayWeb3StatementProps) { + const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const attributes = extractAttributesFromCredentialSubject(statements, credential); + const header = t('headers.reveal'); + + const lines: StatementLine[] = statements.map((s) => { + const { name, value } = attributes[s.attributeTag]; + + return { + attribute: name, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + }; + }); + + return ; +} + +export function DisplayWeb3SecretStatement({ + statements, + dappName, + credential, + className, +}: DisplayWeb3StatementProps) { + const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const { name, value } = extractAttributesFromCredentialSubjectForSingleStatement(statements, credential); + const header = t('headers.reveal'); + + const lines: StatementLine[] = [ + { + attribute: name, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + }, + ]; + + return ; +} + +export default function DisplayWeb3Statement({ + credentialStatement, + dappName, + setChosenId, + net, +}: DisplayCredentialStatementProps) { + const reveals = credentialStatement.statement.filter( + (s) => s.type === StatementTypes.RevealAttribute + ) as RevealStatementV2[]; + const secrets = credentialStatement.statement.filter( + (s) => s.type !== StatementTypes.RevealAttribute + ) as SecretStatementV2[]; + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + + const validCredentials = useMemo(() => { + if (!verifiableCredentials) { + return []; + } + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials); + }, [credentialStatement.idQualifier.issuers]); + + const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + + const onChange = useCallback((credential: VerifiableCredential) => { + setChosenCredential(credential); + setChosenId(createWeb3IdDIDFromCredential(credential, net)); + }, []); + + // Initially set chosenId + useEffect(() => { + if (chosenCredential) { + setChosenId(createWeb3IdDIDFromCredential(chosenCredential, net)); + } + }, []); + + const schema = useMemo(() => { + if (!verifiableCredentialSchemas.loading && chosenCredential) { + const schemaId = chosenCredential.credentialSchema.id; + return verifiableCredentialSchemas.value[schemaId]; + } + return null; + }, [chosenCredential?.id, verifiableCredentials?.length]); + + if (!chosenCredential || !schema) { + return null; + } + + return ( +
    + getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} + onChange={onChange} + /> + {reveals.length !== 0 && ( + + )} + {secrets.map((s, i) => ( + + ))} +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss new file mode 100644 index 000000000..a9c629417 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -0,0 +1,19 @@ +.web3-id-proof-request { + &__statement-container { + display: flex; + flex-direction: column; + height: 100%; + } + + &__actions { + margin: auto rem(20px) 0; + } + + &__loading-icon { + max-height: rem(20px); + } + + &__credential-statement-container { + margin-bottom: rem(20px); + } +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx new file mode 100644 index 000000000..953137b28 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { useTranslation } from 'react-i18next'; +import { + CredentialStatements, + RequestStatement, + ConcordiumHdWallet, + isAccountCredentialStatement, + Web3IdProofInput, +} from '@concordium/web-sdk'; +import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; + +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; +import { credentialsAtom } from '@popup/store/account'; +import { addToastAtom } from '@popup/state'; +import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; +import { getGlobal, getNet } from '@shared/utils/network-helpers'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; +import PendingArrows from '@assets/svg/pending-arrows.svg'; +import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; +import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; +import Button from '@popup/shared/Button'; +import ButtonGroup from '@popup/shared/ButtonGroup'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { + storedVerifiableCredentialsAtom, + storedVerifiableCredentialSchemasAtom, +} from '@popup/store/verifiable-credential'; +import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; +import { DisplayCredentialStatement } from './DisplayStatement'; +import { getCommitmentInput } from './utils'; + +type Props = { + onSubmit(presentationString: string): void; + onReject(): void; +}; + +interface Location { + state: { + payload: { + challenge: string; + statements: CredentialStatements; + url: string; + }; + }; +} + +export default function Web3ProofRequest({ onReject, onSubmit }: Props) { + const { state } = useLocation() as Location; + const { statements, challenge, url } = state.payload; + const { onClose, withClose } = useContext(fullscreenPromptContext); + const { t } = useTranslation('idProofRequest'); + const network = useAtomValue(networkConfigurationAtom); + const client = useAtomValue(grpcClientAtom); + const addToast = useSetAtom(addToastAtom); + const recoveryPhrase = useDecryptedSeedPhrase((e) => addToast(e.message)); + const dappName = displayUrl(url); + const [creatingProof, setCreatingProof] = useState(false); + const net = getNet(network); + + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const identities = useConfirmedIdentities(); + const credentials = useAtomValue(credentialsAtom); + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + + const [ids, setIds] = useState(statements.map(() => '')); + + const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); + + const handleSubmit = useCallback(async () => { + if (!recoveryPhrase) { + throw new Error('Missing recovery phrase'); + } + if (!network) { + throw new Error('Network is not specified'); + } + if (!ids.every((x) => Boolean(x))) { + throw new Error('Network is not specified'); + } + + const global = await getGlobal(client); + const wallet = ConcordiumHdWallet.fromHex(recoveryPhrase, net); + + const type = ['ConcordiumVerifiableCredential', 'TestCredential', 'VerifiableCredential']; + + const parsedStatements: RequestStatement[] = statements.map((statement, index) => { + if (isAccountCredentialStatement(statement)) { + return { statement: statement.statement, id: ids[index] }; + } + return { statement: statement.statement, id: ids[index], type }; + }); + + const commitmentInputs = parsedStatements.map((statement) => + getCommitmentInput( + statement, + wallet, + identities.value, + credentials, + verifiableCredentials || [], + verifiableCredentialSchemas.value + ) + ); + + const request = { + challenge, + credentialStatements: parsedStatements, + }; + + const input: Web3IdProofInput = { + request, + commitmentInputs, + globalContext: global, + }; + + const result: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( + InternalMessageType.CreateWeb3Proof, + input + ); + + if (result.status !== BackgroundResponseStatus.Success) { + throw new Error(result.reason); + } + return result.proof; + }, [recoveryPhrase, network, ids]); + + useEffect(() => onClose(onReject), [onClose, onReject]); + + if (verifiableCredentialSchemas.loading || identities.loading) { + return null; + } + + return ( + +
    + {statements.map((s, index) => ( + + setIds((currentIds) => { + const newIds = [...currentIds]; + newIds[index] = newId; + return newIds; + }) + } + /> + ))} + + + + +
    +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts new file mode 100644 index 000000000..3898bfa24 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts @@ -0,0 +1 @@ +export { default } from './Web3ProofRequest'; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts new file mode 100644 index 000000000..e2d31037b --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -0,0 +1,164 @@ +import { + RequestStatement, + canProveCredentialStatement, + ConcordiumHdWallet, + createWeb3CommitmentInputWithHdWallet, + createAccountCommitmentInputWithHdWallet, + VerifiableCredentialStatement, + AccountCredentialStatement, + Network, + AtomicStatementV2, + RevealStatementV2, + ContractAddress, + CommitmentInput, + VerifiableCredentialSchema, + createWeb3IdDID, +} from '@concordium/web-sdk'; +import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; +import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; +import { ClassName } from 'wallet-common-helpers'; + +export type SecretStatementV2 = Exclude; + +export interface DisplayCredentialStatementProps extends ClassName { + credentialStatement: Statement; + dappName: string; + setChosenId: (id: string) => void; + net: Network; +} + +/** Takes a Web3IdCredential issuer DID string and returns the contract address + * @param did a issuer DID string on the form: "did:ccd:testnet:sci:INDEX:SUBINDEX/issuer" + * @returns the contract address INDEX;SUBINDEX + */ +export function getContractAddressFromIssuerDID(did: string): ContractAddress { + const split = did.split(':'); + if (split.length !== 6 || split[3] !== 'sci') { + throw new Error('Given DID did not follow expected format'); + } + const index = BigInt(split[4]); + const subindex = BigInt(split[5].substring(0, split[5].indexOf('/'))); + return { index, subindex }; +} + +/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential + * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" + * @returns the public key KEY + */ +export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { + const split = did.split('/'); + if (split.length !== 3 || split[0].split(':')[3] !== 'sci') { + throw new Error(`Given DID did not follow expected format:${did}`); + } + return split[2]; +} + +/** Takes a AccountCredential subject DID string and returns the credential id of the account credential + * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" + * @returns the credId CREDID + */ +export function getCredentialIdFromSubjectDID(did: string) { + const split = did.split(':'); + if (split.length !== 5 || split[3] !== 'cred') { + throw new Error(`Given DID did not follow expected format: ${did}`); + } + return split[4]; +} + +/** + * Build the commitmentInputs required to create a presentation for the given statement. + */ +export function getCommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + identities: ConfirmedIdentity[], + credentials: WalletCredential[], + verifiableCredentials: VerifiableCredential[], + verifiableCredentialSchemas: Record +): CommitmentInput { + if (statement.type) { + const cred = verifiableCredentials?.find((c) => c.id === statement.id); + + if (!cred) { + throw new Error('IdQualifier not fulfilled'); + } + + const schemaIndex = cred.credentialSchema.id; + + return createWeb3CommitmentInputWithHdWallet( + wallet, + getContractAddressFromIssuerDID(cred.issuer), + cred.index, + cred.credentialSubject, + verifiableCredentialSchemas[schemaIndex], + cred.randomness, + cred.signature + ); + } + const credId = getCredentialIdFromSubjectDID(statement.id); + const credential = credentials.find((cred) => cred.credId === credId); + + if (!credential) { + throw new Error('IdQualifier not fulfilled'); + } + + const identity = (identities || []).find(isIdentityOfCredential); + + if (!identity || identity.status !== CreationStatus.Confirmed) { + throw new Error('No identity found for credential'); + } + + return createAccountCommitmentInputWithHdWallet( + statement.statement, + identity.providerIndex, + identity.idObject.value.attributeList, + wallet, + identity.providerIndex, + credential.credNumber + ); +} + +/** + * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. + * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. + */ +export function getViableAccountCredentialsForStatement( + credentialStatement: AccountCredentialStatement, + identities: ConfirmedIdentity[], + credentials: WalletCredential[] +): WalletCredential[] { + const allowedIssuers = credentialStatement.idQualifier.issuers; + return credentials?.filter((c) => { + if (allowedIssuers.includes(c.providerIndex)) { + const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); + if (identity && identity.status === CreationStatus.Confirmed) { + return canProveCredentialStatement(credentialStatement, identity.idObject.value.attributeList); + } + } + return false; + }); +} + +/** + * Given a credential statement for a verifiable credential, and a list of verifiable credentials, return the filtered list of verifiable credentials that satisfy the statement. + */ +export function getViableWeb3IdCredentialsForStatement( + credentialStatement: VerifiableCredentialStatement, + verifiableCredentials: VerifiableCredential[] +): VerifiableCredential[] { + const allowedContracts = credentialStatement.idQualifier.issuers; + return verifiableCredentials?.filter((vc) => + allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) + ); +} + +// TODO move to SDK? +export function createWeb3IdDIDFromCredential(credential: VerifiableCredential, net: Network) { + const contractAddress = getContractAddressFromIssuerDID(credential.issuer); + return createWeb3IdDID( + net, + getVerifiableCredentialPublicKeyfromSubjectDID(credential.id), + BigInt(contractAddress.index), + BigInt(contractAddress.subindex) + ); +} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index c7ddd0b24..5edf8bcc9 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -33,6 +33,7 @@ import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; +import Web3ProofRequest from '@popup/pages/Web3ProofRequest'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; @@ -110,6 +111,11 @@ export default function Routes() { InternalMessageType.IdProof, 'idProof' ); + // We manually stringify the presentation + const handleWeb3IdProofResponse = useMessagePrompt>( + InternalMessageType.Web3Proof, + 'web3IdProof' + ); usePrompt(InternalMessageType.EndIdentityIssuance, 'endIdentityIssuance'); usePrompt(InternalMessageType.RecoveryFinished, 'recovery'); @@ -201,6 +207,19 @@ export default function Routes() { /> } /> + + handleWeb3IdProofResponse({ success: true, result: presentationString }) + } + onReject={() => + handleWeb3IdProofResponse({ success: false, message: 'Proof generation was rejected' }) + } + /> + } + /> } /> } /> diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index f073dead6..517b0c7b0 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -42,6 +42,7 @@ @import '../pages/TermsAndConditions/TermsAndConditions'; @import '../pages/IdProofRequest'; @import '../pages/AddWeb3IdCredential/AddWeb3IdCredential'; +@import '../pages/Web3ProofRequest/Web3ProofRequest'; // Layouts @import '../page-layouts/MainLayout'; diff --git a/packages/browser-wallet/src/shared/utils/proof-helpers.ts b/packages/browser-wallet/src/shared/utils/proof-helpers.ts new file mode 100644 index 000000000..349afb6ce --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/proof-helpers.ts @@ -0,0 +1,19 @@ +import { AtomicStatement, StatementTypes } from '@concordium/web-sdk'; +import { ConfirmedIdentity } from '@shared/storage/types'; + +export function canProveStatement(statement: AtomicStatement, identity: ConfirmedIdentity) { + const attribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag]; + + switch (statement.type) { + case StatementTypes.AttributeInSet: + return statement.set.includes(attribute); + case StatementTypes.AttributeNotInSet: + return !statement.set.includes(attribute); + case StatementTypes.AttributeInRange: + return statement.upper > attribute && attribute >= statement.lower; + case StatementTypes.RevealAttribute: + return attribute !== undefined; + default: + throw new Error(`Statement type of ${statement.type} is not supported`); + } +} diff --git a/packages/browser-wallet/src/shared/utils/types.ts b/packages/browser-wallet/src/shared/utils/types.ts index 6d724be2a..66627f051 100644 --- a/packages/browser-wallet/src/shared/utils/types.ts +++ b/packages/browser-wallet/src/shared/utils/types.ts @@ -3,7 +3,7 @@ import { SchemaWithContext, SmartContractParameters, } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; -import type { IdProofOutput, SchemaVersion, AccountTransactionType } from '@concordium/web-sdk'; +import type { SchemaVersion, AccountTransactionType } from '@concordium/web-sdk'; import { RefAttributes } from 'react'; /** * @description @@ -42,10 +42,10 @@ export type CredentialDeploymentBackgroundResponse = address: string; }; -export type IdProofBackgroundResponse = +export type ProofBackgroundResponse = | { status: BackgroundResponseStatus.Success; - proof: IdProofOutput; + proof: ProofOutput; } | { status: BackgroundResponseStatus.Error; diff --git a/packages/browser-wallet/test/Web3ProofRequest.test.ts b/packages/browser-wallet/test/Web3ProofRequest.test.ts new file mode 100644 index 000000000..670e367e3 --- /dev/null +++ b/packages/browser-wallet/test/Web3ProofRequest.test.ts @@ -0,0 +1,27 @@ +import { + getCredentialIdFromSubjectDID, + getContractAddressFromIssuerDID, + getVerifiableCredentialPublicKeyfromSubjectDID, +} from '../src/popup/pages/Web3ProofRequest/utils'; + +test('getContractAddressFromIssuerDID', () => { + const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); + expect(address.index).toBe(1337n); + expect(address.subindex).toBe(42n); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); From 5eec88017532c032a373dece665468bc00f1684c Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 11 Aug 2023 14:28:51 +0200 Subject: [PATCH 02/19] Incomplete fixes --- examples/add-example-Web3Id/index.html | 2 +- packages/browser-wallet-api/src/wallet-api.ts | 5 +- .../browser-wallet-message-hub/src/message.ts | 6 +- packages/browser-wallet/package.json | 2 +- .../browser-wallet/src/background/id-proof.ts | 38 ------------ .../browser-wallet/src/background/index.ts | 10 ++-- .../browser-wallet/src/background/web3Id.ts | 58 ++++++++++++++++++- .../src/popup/pages/IdProofRequest/i18n/da.ts | 1 + .../src/popup/pages/IdProofRequest/i18n/en.ts | 1 + .../VerifiableCredentialStatement.tsx | 52 +++++++++++++---- .../Web3ProofRequest/Web3ProofRequest.tsx | 15 +++-- .../popup/pages/Web3ProofRequest/i18n/en.ts | 34 +++++++++++ .../src/popup/pages/Web3ProofRequest/utils.ts | 10 ++-- .../browser-wallet/src/popup/shell/Routes.tsx | 2 +- .../src/popup/shell/i18n/locales/en.ts | 2 + 15 files changed, 161 insertions(+), 77 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index 654af9e36..f8e07d3ca 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -22,7 +22,7 @@ provider.on('chainChanged', (chain) => alert(chain)); document.getElementById('web3Proof').addEventListener('click', () => { const statement = new concordiumSDK.Web3StatementBuilder() - .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => b .revealAttribute('graduationDate') .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 8dfe369d6..ca04ecd56 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -289,8 +289,9 @@ class WalletApi extends EventEmitter implements IWalletApi { } public async requestVerifiablePresentation(challenge: HexString, statements: CredentialStatements) { - const res = await this.messageHandler.sendMessage>(MessageType.Web3Proof, { - statements, + const res = await this.messageHandler.sendMessage>(MessageType.Web3IdProof, { + // We have to stringify the statements because they can contain bigints + statements: stringify(statements), challenge, }); diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 5e1c65a21..6e2dcfea8 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -16,7 +16,7 @@ export enum MessageType { GrpcRequest = 'M_GrpcRequest', AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', - Web3Proof = 'M_Web3Proof', + Web3IdProof = 'M_Web3Proof', ConnectAccounts = 'M_ConnectAccounts', AddWeb3IdCredential = 'M_AddWeb3IdCredential', AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish', @@ -41,8 +41,8 @@ export enum InternalMessageType { AddTokens = 'I_AddTokens', IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', - Web3Proof = 'I_Web3Proof', - CreateWeb3Proof = 'I_CreateWeb3Proof', + Web3IdProof = 'I_Web3IdProof', + CreateWeb3IdProof = 'I_CreateWeb3IdProof', ConnectAccounts = 'I_ConnectAccounts', AddWeb3IdCredential = 'I_AddWeb3IdCredential', } diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 5867c6efc..e8977fbc8 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0", + "version": "1.1.0.2", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index 1fbb1071d..d8b9a4463 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,10 +1,8 @@ import { getIdProof, - getVerifiablePresentation, IdProofInput, IdProofOutput, verifyIdstatement, - Web3IdProofInput, } from '@concordium/web-sdk'; import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; @@ -46,39 +44,3 @@ export const runIfValidProof: RunCondition> = as }; } }; - -async function createWeb3Proof(input: Web3IdProofInput): Promise> { - const proof = getVerifiablePresentation(input); - return { - status: BackgroundResponseStatus.Success, - proof: proof.toString(), - }; -} - -export const createWeb3ProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { - createWeb3Proof(msg.payload) - .then(respond) - .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); - return true; -}; - -/** - * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. - */ -export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { - if (!isHex(msg.payload.challenge)) { - return { - run: false, - response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, - }; - } - try { - // TODO web3: Check that the request is well-formed - return { run: true }; - } catch (e) { - return { - run: false, - response: { success: false, message: `Id statement is not well-formed: ${(e as Error).message}` }, - }; - } -}; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index b38289ea1..60421e4f3 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -26,7 +26,7 @@ import { Buffer } from 'buffer/'; import JSONBig from 'json-bigint'; import { startMonitoringPendingStatus } from './confirmation'; import { sendCredentialHandler } from './credential-deployment'; -import { createIdProofHandler, createWeb3ProofHandler, runIfValidProof, runIfValidWeb3IdProof } from './id-proof'; +import { createIdProofHandler, runIfValidProof } from './id-proof'; import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; import bgMessageHandler from './message-handler'; import { setupRecoveryHandler, startRecovery } from './recovery'; @@ -39,7 +39,7 @@ import { setPopupSize, testPopupOpen, } from './window-management'; -import { runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler } from './web3Id'; +import {runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof } from './web3Id'; const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -252,7 +252,7 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); -bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3Proof), createWeb3ProofHandler); +bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), createWeb3IdProofHandler); const NOT_WHITELISTED = 'Site is not whitelisted'; @@ -591,8 +591,8 @@ forwardToPopup( ); forwardToPopup( - MessageType.Web3Proof, - InternalMessageType.Web3Proof, + MessageType.Web3IdProof, + InternalMessageType.Web3IdProof, runConditionComposer(runIfAllowlisted, runIfValidWeb3IdProof, withPromptStart()), appendUrlToPayload, undefined, diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 4e7e43eef..ce3f6af91 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -1,4 +1,9 @@ -import { createConcordiumClient, verifyWeb3IdCredentialSignature } from '@concordium/web-sdk'; +import { + CredentialStatements, + getVerifiablePresentation, + Web3IdProofInput, + createConcordiumClient, verifyWeb3IdCredentialSignature, isHex, verifyAtomicStatements +} from '@concordium/web-sdk'; import { sessionVerifiableCredentials, storedCurrentNetwork, @@ -16,8 +21,10 @@ import { getDIDNetwork, getPublicKeyfromPublicKeyIdentifierDID, } from '@shared/utils/verifiable-credential-helpers'; -import { MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; +import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { getNet } from '@shared/utils/network-helpers'; +import { parse } from '@shared/utils/payload-helpers'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { RunCondition } from './window-management'; const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; @@ -132,3 +139,50 @@ export const runIfValidWeb3IdCredentialRequest: RunCondition> { + const proof = getVerifiablePresentation(input); + return { + status: BackgroundResponseStatus.Success, + proof: proof.toString(), + }; +} + +export const createWeb3IdProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { + createWeb3Proof(msg.payload) + .then(respond) + .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); + return true; +}; + +/** + * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. + */ +export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { + if (!isHex(msg.payload.challenge)) { + return { + run: false, + response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, + }; + } + try { + const statements: CredentialStatements = parse(msg.payload.statements); + // TODO Fix second parameter when SDK is updated + // If a statement does not verify, an error is thrown. + statements.every((credStatement) => verifyAtomicStatements(credStatement.statement, undefined as any)); + + const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); + if (!noEmptyQualifier) { + return { + run: false, + response: { success: false, message: `Statements must have at least 1 possible identity provider / issuer` }, + }; + } + return { run: true }; + } catch (e) { + return { + run: false, + response: { success: false, message: `Statement is not well-formed: ${(e as Error).message}` }, + }; + } +}; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts index 2da97ad1a..33752ddd0 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts @@ -26,6 +26,7 @@ const da: typeof en = { residence: 'Zero Knowledge bevis for bopælsland', idDocType: 'Zero Knowledge bevis for identitetsdokumenttype', idDocIssuer: 'Zero Knowledge bevis for identitetsdokumentudsteder', + secret: 'Zero Knowledge bevis' }, names: { age: 'Alder', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts index 1d0ec3d0e..7cdaf90a1 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts @@ -24,6 +24,7 @@ export default { residence: 'Zero Knowledge proof of country of residence', idDocType: 'Zero Knowledge proof of identity document type', idDocIssuer: 'Zero Knowledge proof of identity document issuer', + secret: 'Zero Knowledge proof' }, names: { age: 'Age', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 4d6939987..67469469f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -8,7 +8,7 @@ import { storedVerifiableCredentialsAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, CredentialSubject } from '@shared/storage/types'; +import { VerifiableCredential, CredentialSubject, VerifiableCredentialSchema } from '@shared/storage/types'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,10 +23,31 @@ import { SecretStatementV2, } from './utils'; +function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { + const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; + return property.title; +} + +function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { + const { t } = useTranslation('Web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); + + const name = getPropertyTitle(statement.attributeTag, schema); + if (statement.type === StatementTypes.AttributeInRange) { + return t('range', { name, upper: statement.upper, lower: statement.lower }); + } else if (statement.type === StatementTypes.AttributeInSet) { + return t('membership', { name }); + } else if (statement.type === StatementTypes.AttributeNotInSet) { + return t('nonMembership', { name }); + } + // TODO What to do here? + return ''; +} + type DisplayWeb3StatementProps = ClassName & { statements: Statement; dappName: string; credential: CredentialSubject; + schema: VerifiableCredentialSchema; }; type AttributeInfo = { @@ -38,7 +59,7 @@ function extractAttributesFromCredentialSubjectForSingleStatement( { attributeTag }: AtomicStatementV2, credentialSubject: CredentialSubject ): AttributeInfo { - return { name: attributeTag, value: credentialSubject[attributeTag] }; + return { name: attributeTag, value: credentialSubject.attributes[attributeTag] }; } function extractAttributesFromCredentialSubject( @@ -59,16 +80,17 @@ export function DisplayWeb3RevealStatement({ dappName, credential, className, + schema }: DisplayWeb3StatementProps) { const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); const attributes = extractAttributesFromCredentialSubject(statements, credential); const header = t('headers.reveal'); const lines: StatementLine[] = statements.map((s) => { - const { name, value } = attributes[s.attributeTag]; - + const { value } = attributes[s.attributeTag]; + const property = schema.properties.credentialSubject.properties.attributes.properties[s.attributeTag]; return { - attribute: name, + attribute: property.title, value: value.toString() ?? 'Unavailable', isRequirementMet: value !== undefined, }; @@ -82,19 +104,23 @@ export function DisplayWeb3SecretStatement({ dappName, credential, className, + schema }: DisplayWeb3StatementProps) { const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); - const { name, value } = extractAttributesFromCredentialSubjectForSingleStatement(statements, credential); - const header = t('headers.reveal'); + // Fix double name for membership + const value = useStatementValue(statements, schema); + const header = t('headers.secret'); + const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; const lines: StatementLine[] = [ { - attribute: name, - value: value.toString() ?? 'Unavailable', + attribute: property.title, + value, isRequirementMet: value !== undefined, }, ]; + // TODO Add description / list of options for membership + check range return ; } @@ -147,7 +173,7 @@ export default function DisplayWeb3Statement({ } return ( -
    +
    getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} @@ -158,7 +184,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={reveals} + statements={reveals} + schema={schema} /> )} {secrets.map((s, i) => ( @@ -168,7 +195,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={s} + statements={s} + schema={schema} /> ))}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 953137b28..f8f07d68b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -31,6 +31,7 @@ import { import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; import { DisplayCredentialStatement } from './DisplayStatement'; import { getCommitmentInput } from './utils'; +import { parse } from '@shared/utils/payload-helpers'; type Props = { onSubmit(presentationString: string): void; @@ -41,7 +42,7 @@ interface Location { state: { payload: { challenge: string; - statements: CredentialStatements; + statements: string; url: string; }; }; @@ -49,7 +50,7 @@ interface Location { export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; - const { statements, challenge, url } = state.payload; + const { statements: rawStatements, challenge, url } = state.payload; const { onClose, withClose } = useContext(fullscreenPromptContext); const { t } = useTranslation('idProofRequest'); const network = useAtomValue(networkConfigurationAtom); @@ -60,13 +61,15 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const [creatingProof, setCreatingProof] = useState(false); const net = getNet(network); + const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]) + + const [ids, setIds] = useState(Array(statements.length).fill('')); + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const identities = useConfirmedIdentities(); const credentials = useAtomValue(credentialsAtom); const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); - const [ids, setIds] = useState(statements.map(() => '')); - const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); const handleSubmit = useCallback(async () => { @@ -92,6 +95,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return { statement: statement.statement, id: ids[index], type }; }); + const commitmentInputs = parsedStatements.map((statement) => getCommitmentInput( statement, @@ -99,7 +103,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { identities.value, credentials, verifiableCredentials || [], - verifiableCredentialSchemas.value ) ); @@ -115,7 +118,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }; const result: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( - InternalMessageType.CreateWeb3Proof, + InternalMessageType.CreateWeb3IdProof, input ); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts new file mode 100644 index 000000000..cacfd156b --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -0,0 +1,34 @@ +export default { + header: '{{dappName}} requests the following information about you:', + accept: 'Accept', + reject: 'Reject', + displayStatement: { + requirementsMet: 'You meet this requirement', + requirementsNotMet: "You don't meet this requirement", + revealDescription: + '<1>Important: {{dappName}} will be given all the information above. You should only accept, if you trust the service, and you are familiar with their privacy policy.', + revealTooltip: { + header: 'Revealing information <1 />', + body: 'When you reveal information to a third party, you effectively hand over the information to them. This means you should only do this if you agree to their data usage and protection policies.\n\nYou can read more in\n<1>the developer documentation.', + }, + secretTooltip: { + header: 'Zero Knowledge proofs', + body: 'Zero Knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact age. Another example could be proving your residency is within a given set of countries without revealing which of those countries you reside within.\n\nYou can read more in\n<1>the developer documentation.', + }, + headers: { + reveal: 'Information to reveal', + secret: 'Zero Knowledge proof' + }, + proofs: { + range: '{{ name }} is between {{ lower }} and {{ upper }}', + membership: '{{ name }} is 1 of the following', + nonMembership: '{{ name }} is none of the following', + }, + descriptions: { + + missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', + }, + }, + failedProof: 'Unable to create proof', + failedProofReason: 'Unable to create proof due to: {{ reason }}', +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index e2d31037b..0e3d75057 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -11,7 +11,6 @@ import { RevealStatementV2, ContractAddress, CommitmentInput, - VerifiableCredentialSchema, createWeb3IdDID, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; @@ -74,7 +73,6 @@ export function getCommitmentInput( identities: ConfirmedIdentity[], credentials: WalletCredential[], verifiableCredentials: VerifiableCredential[], - verifiableCredentialSchemas: Record ): CommitmentInput { if (statement.type) { const cred = verifiableCredentials?.find((c) => c.id === statement.id); @@ -83,14 +81,11 @@ export function getCommitmentInput( throw new Error('IdQualifier not fulfilled'); } - const schemaIndex = cred.credentialSchema.id; - return createWeb3CommitmentInputWithHdWallet( wallet, getContractAddressFromIssuerDID(cred.issuer), cred.index, cred.credentialSubject, - verifiableCredentialSchemas[schemaIndex], cred.randomness, cred.signature ); @@ -146,13 +141,16 @@ export function getViableWeb3IdCredentialsForStatement( credentialStatement: VerifiableCredentialStatement, verifiableCredentials: VerifiableCredential[] ): VerifiableCredential[] { + // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; return verifiableCredentials?.filter((vc) => allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) ); } -// TODO move to SDK? +/** + * Helper function to create a web3Id DID string from a verifiable credential + */ export function createWeb3IdDIDFromCredential(credential: VerifiableCredential, net: Network) { const contractAddress = getContractAddressFromIssuerDID(credential.issuer); return createWeb3IdDID( diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 5edf8bcc9..f6c3eda07 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -113,7 +113,7 @@ export default function Routes() { ); // We manually stringify the presentation const handleWeb3IdProofResponse = useMessagePrompt>( - InternalMessageType.Web3Proof, + InternalMessageType.Web3IdProof, 'web3IdProof' ); diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 93aa0f1c2..978674cdb 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -21,6 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; +import Web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; const t = { shared, @@ -46,6 +47,7 @@ const t = { allowlist, connectAccountsRequest, addWeb3IdCredential, + Web3IdProofRequest }; export default t; From 5376d1e4f6ed9c4cd4c37267a2c040ed170cef6b Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 10:23:38 +0200 Subject: [PATCH 03/19] Fix display of web3Id proof statement view --- examples/add-example-Web3Id/index.html | 3 +- .../VerifiableCredentialStatement.tsx | 63 +++++++++++++------ .../popup/pages/Web3ProofRequest/i18n/en.ts | 14 +++-- .../src/popup/shell/i18n/locales/da.ts | 3 + .../src/popup/shell/i18n/locales/en.ts | 4 +- 5 files changed, 60 insertions(+), 27 deletions(-) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index f8e07d3ca..e07a37497 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -24,7 +24,8 @@ const statement = new concordiumSDK.Web3StatementBuilder() .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => b - .revealAttribute('graduationDate') + .revealAttribute('degreeType') + .addRange('graduationDate', '2000-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) ) .getStatements(); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 67469469f..cfdb1890b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -29,24 +29,42 @@ function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSche } function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { - const { t } = useTranslation('Web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); const name = getPropertyTitle(statement.attributeTag, schema); if (statement.type === StatementTypes.AttributeInRange) { return t('range', { name, upper: statement.upper, lower: statement.lower }); - } else if (statement.type === StatementTypes.AttributeInSet) { + } + if (statement.type === StatementTypes.AttributeInSet) { return t('membership', { name }); - } else if (statement.type === StatementTypes.AttributeNotInSet) { + } + if (statement.type === StatementTypes.AttributeNotInSet) { return t('nonMembership', { name }); } // TODO What to do here? return ''; } +export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + const name = getPropertyTitle(statement.attributeTag, schema); + const listToString = (list: (string | bigint)[]) => list.map((member) => member.toString()).join(', '); + + switch (statement.type) { + case StatementTypes.AttributeInRange: + return t('range', { name, lower: statement.lower, upper: statement.upper }); + case StatementTypes.AttributeInSet: + return t('membership', { name, setNames: listToString(statement.set) }); + case StatementTypes.AttributeNotInSet: + return t('nonMembership', { name, setNames: listToString(statement.set) }); + default: + return undefined; + } +} + type DisplayWeb3StatementProps = ClassName & { statements: Statement; dappName: string; - credential: CredentialSubject; schema: VerifiableCredentialSchema; }; @@ -75,14 +93,18 @@ function extractAttributesFromCredentialSubject( }, {}); } +type DisplayWeb3RevealStatementProps = DisplayWeb3StatementProps & { + credential: CredentialSubject; +}; + export function DisplayWeb3RevealStatement({ statements, dappName, credential, className, - schema -}: DisplayWeb3StatementProps) { - const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + schema, +}: DisplayWeb3RevealStatementProps) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const attributes = extractAttributesFromCredentialSubject(statements, credential); const header = t('headers.reveal'); @@ -102,15 +124,14 @@ export function DisplayWeb3RevealStatement({ export function DisplayWeb3SecretStatement({ statements, dappName, - credential, className, - schema + schema, }: DisplayWeb3StatementProps) { - const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); - // Fix double name for membership + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const value = useStatementValue(statements, schema); const header = t('headers.secret'); const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; + const description = useStatementDescription(statements, schema); const lines: StatementLine[] = [ { @@ -120,8 +141,15 @@ export function DisplayWeb3SecretStatement({ }, ]; - // TODO Add description / list of options for membership + check range - return ; + return ( + + ); } export default function DisplayWeb3Statement({ @@ -184,8 +212,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={reveals} - schema={schema} + statements={reveals} + schema={schema} /> )} {secrets.map((s, i) => ( @@ -194,9 +222,8 @@ export default function DisplayWeb3Statement({ key={i} // Allow this, as we don't expect these to ever change. className="m-t-10:not-first" dappName={dappName} - credential={chosenCredential.credentialSubject} - statements={s} - schema={schema} + statements={s} + schema={schema} /> ))}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index cacfd156b..42a31f55d 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -17,18 +17,20 @@ export default { }, headers: { reveal: 'Information to reveal', - secret: 'Zero Knowledge proof' + secret: 'Zero Knowledge proof', }, proofs: { - range: '{{ name }} is between {{ lower }} and {{ upper }}', - membership: '{{ name }} is 1 of the following', - nonMembership: '{{ name }} is none of the following', + range: 'Between {{ lower }} and {{ upper }}', + membership: '1 of the following', + nonMembership: 'None of the following', }, descriptions: { - + range: 'This will prove that your {{ name }} is between {{ lower }} and {{ upper }}', + membership: 'This will prove that your {{ name }} is 1 of the following:\n{{ setNames }}', + nonMembership: 'This will prove that your {{ name }} is none of the following:\n{{ setNames }}', missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', }, }, failedProof: 'Unable to create proof', failedProofReason: 'Unable to create proof due to: {{ reason }}', -} +}; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 82bcc0bd9..bc6f5dd02 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -21,6 +21,8 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; import allowlist from '@popup/pages/Allowlist/i18n/da'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/da'; +// TODO add "da" version +import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; import type en from './en'; @@ -48,6 +50,7 @@ const t: typeof en = { allowlist, connectAccountsRequest, addWeb3IdCredential, + web3IdProofRequest, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 978674cdb..136b4de8d 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -21,7 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; -import Web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; +import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; const t = { shared, @@ -47,7 +47,7 @@ const t = { allowlist, connectAccountsRequest, addWeb3IdCredential, - Web3IdProofRequest + web3IdProofRequest, }; export default t; From 872130bf557ff562325e3745e6701a8f6ab2228b Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 13:33:13 +0200 Subject: [PATCH 04/19] First example --- examples/two-step-transfer/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/two-step-transfer/index.html b/examples/two-step-transfer/index.html index 51cf100e8..b1841f30d 100644 --- a/examples/two-step-transfer/index.html +++ b/examples/two-step-transfer/index.html @@ -217,10 +217,10 @@ document.getElementById('web3Proof').addEventListener('click', () => { const statement = new concordiumSDK.Web3StatementBuilder() .addForIdentityCredentials([0, 1, 2], (b) => - b.revealAttribute(0).addRange(3, '08000101', '20000101') + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') ) .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => - b.revealAttribute(0).addMembership(2, ['2010-06-01T00:00:00Z']) + b.revealAttribute('degreeName').addMembership('graduationDate', ['2010-06-01T00:00:00Z']) ) .getStatements(); const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; From e28c76989e27789d6b54b0a76dff315b45af895d Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 15:39:11 +0200 Subject: [PATCH 05/19] Display VC card in proof + paging --- .../VerifiableCredentialHooks.tsx | 20 ++-- .../VerifiableCredentialStatement.tsx | 20 +++- .../Web3ProofRequest/Web3ProofRequest.tsx | 97 +++++++++---------- .../popup/pages/Web3ProofRequest/i18n/en.ts | 1 + 4 files changed, 78 insertions(+), 60 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 792f9629e..92fc85db3 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -64,17 +64,19 @@ export function useCredentialSchema(credential: VerifiableCredential) { * @param credential the verifiable credential to retrieve the credential entry for * @returns the credential entry for the given credential, undefined if one is not found yet */ -export function useCredentialEntry(credential: VerifiableCredential) { +export function useCredentialEntry(credential?: VerifiableCredential) { const [credentialEntry, setCredentialEntry] = useState(); const client = useAtomValue(grpcClientAtom); useEffect(() => { - const credentialHolderId = getCredentialHolderId(credential.id); - const registryContractAddress = getCredentialRegistryContractAddress(credential.id); - getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { - setCredentialEntry(entry); - }); - }, [credential.id, client]); + if (credential) { + const credentialHolderId = getCredentialHolderId(credential.id); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { + setCredentialEntry(entry); + }); + } + }, [credential?.id, client]); return credentialEntry; } @@ -86,7 +88,7 @@ export function useCredentialEntry(credential: VerifiableCredential) { * @throws if no credential metadata is found in storage for the provided credential * @returns the credential's metadata used for rendering the credential */ -export function useCredentialMetadata(credential: VerifiableCredential) { +export function useCredentialMetadata(credential?: VerifiableCredential) { const [metadata, setMetadata] = useState(); const credentialEntry = useCredentialEntry(credential); const storedMetadata = useAtomValue(storedVerifiableCredentialMetadataAtom); @@ -96,7 +98,7 @@ export function useCredentialMetadata(credential: VerifiableCredential) { const storedCredentialMetadata = storedMetadata.value[credentialEntry.credentialInfo.metadataUrl.url]; if (!storedCredentialMetadata) { throw new Error( - `Attempted to find credential metadata for credentialId: ${credential.id} but none was found!` + `Attempted to find credential metadata for credentialId: ${credential?.id} but none was found!` ); } setMetadata(storedCredentialMetadata); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index cfdb1890b..ab40317ba 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -8,12 +8,19 @@ import { storedVerifiableCredentialsAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, CredentialSubject, VerifiableCredentialSchema } from '@shared/storage/types'; +import { + VerifiableCredential, + CredentialSubject, + VerifiableCredentialSchema, + VerifiableCredentialStatus, +} from '@shared/storage/types'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; +import { useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, @@ -196,12 +203,21 @@ export default function DisplayWeb3Statement({ return null; }, [chosenCredential?.id, verifiableCredentials?.length]); - if (!chosenCredential || !schema) { + const metadata = useCredentialMetadata(chosenCredential); + + if (!chosenCredential || !schema || !metadata) { return null; } return (
    + + getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index f8f07d68b..1eadd1d8e 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -29,9 +29,9 @@ import { storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; +import { parse } from '@shared/utils/payload-helpers'; import { DisplayCredentialStatement } from './DisplayStatement'; import { getCommitmentInput } from './utils'; -import { parse } from '@shared/utils/payload-helpers'; type Props = { onSubmit(presentationString: string): void; @@ -52,16 +52,17 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; const { onClose, withClose } = useContext(fullscreenPromptContext); - const { t } = useTranslation('idProofRequest'); + const { t } = useTranslation('web3IdProofRequest'); const network = useAtomValue(networkConfigurationAtom); const client = useAtomValue(grpcClientAtom); const addToast = useSetAtom(addToastAtom); const recoveryPhrase = useDecryptedSeedPhrase((e) => addToast(e.message)); const dappName = displayUrl(url); const [creatingProof, setCreatingProof] = useState(false); + const [currentStatementIndex, setCurrentStatementIndex] = useState(0); const net = getNet(network); - const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]) + const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]); const [ids, setIds] = useState(Array(statements.length).fill('')); @@ -95,15 +96,8 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return { statement: statement.statement, id: ids[index], type }; }); - const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput( - statement, - wallet, - identities.value, - credentials, - verifiableCredentials || [], - ) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials || []) ); const request = { @@ -137,48 +131,53 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return (
    - {statements.map((s, index) => ( - - setIds((currentIds) => { - const newIds = [...currentIds]; - newIds[index] = newId; - return newIds; - }) - } - /> - ))} + + setIds((currentIds) => { + const newIds = [...currentIds]; + newIds[currentStatementIndex] = newId; + return newIds; + }) + } + /> - + {currentStatementIndex === statements.length - 1 ? ( + + ) : ( + + )}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 42a31f55d..f66d695d9 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -1,6 +1,7 @@ export default { header: '{{dappName}} requests the following information about you:', accept: 'Accept', + continue: 'Continue', reject: 'Reject', displayStatement: { requirementsMet: 'You meet this requirement', From 41caf9d1dc0bc6074ae5c9cd72b17304c5cac877 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 09:32:27 +0200 Subject: [PATCH 06/19] Add check for web3IdProof that credential can satisfy statement --- .../VerifiableCredentialStatement.tsx | 4 ++-- .../src/popup/pages/Web3ProofRequest/utils.ts | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index ab40317ba..8ffe86372 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -48,8 +48,8 @@ function useStatementValue(statement: SecretStatementV2, schema: VerifiableCrede if (statement.type === StatementTypes.AttributeNotInSet) { return t('nonMembership', { name }); } - // TODO What to do here? - return ''; + + throw new Error('Unknown statement type'); } export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 0e3d75057..f41924510 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -12,6 +12,7 @@ import { ContractAddress, CommitmentInput, createWeb3IdDID, + StatementTypes, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; @@ -72,7 +73,7 @@ export function getCommitmentInput( wallet: ConcordiumHdWallet, identities: ConfirmedIdentity[], credentials: WalletCredential[], - verifiableCredentials: VerifiableCredential[], + verifiableCredentials: VerifiableCredential[] ): CommitmentInput { if (statement.type) { const cred = verifiableCredentials?.find((c) => c.id === statement.id); @@ -134,6 +135,22 @@ export function getViableAccountCredentialsForStatement( }); } +function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { + const value = cred.credentialSubject.attributes[statement.attributeTag]; + switch (statement.type) { + case StatementTypes.AttributeInRange: + return statement.lower <= value && statement.upper > value; + case StatementTypes.AttributeInSet: + return statement.set.includes(value); + case StatementTypes.AttributeNotInSet: + return !statement.set.includes(value); + case StatementTypes.RevealAttribute: + return value !== undefined; + default: + throw new Error('Unknown statementType encountered'); + } +} + /** * Given a credential statement for a verifiable credential, and a list of verifiable credentials, return the filtered list of verifiable credentials that satisfy the statement. */ @@ -143,9 +160,12 @@ export function getViableWeb3IdCredentialsForStatement( ): VerifiableCredential[] { // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; - return verifiableCredentials?.filter((vc) => + const allowedCredentials = verifiableCredentials?.filter((vc) => allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) ); + return allowedCredentials.filter((cred) => + credentialStatement.statement.every((stm) => doesCredentialSatisfyStatement(stm, cred)) + ); } /** From a92eb0a8d506d86f327a4b81d1ccbf54c605066d Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:27:01 +0200 Subject: [PATCH 07/19] Small fixes --- packages/browser-wallet/package.json | 2 +- .../src/popup/pages/IdProofRequest/i18n/da.ts | 1 - .../src/popup/pages/IdProofRequest/i18n/en.ts | 1 - .../VerifiableCredentialStatement.tsx | 6 +-- .../Web3ProofRequest/Web3ProofRequest.tsx | 6 +-- .../src/popup/pages/Web3ProofRequest/utils.ts | 44 ++--------------- .../utils/verifiable-credential-helpers.ts | 42 +++++++++++++++++ .../test/Web3ProofRequest.test.ts | 27 ----------- .../verifiable-credential-helpers.test.ts | 47 +++++++++++++++++++ 9 files changed, 101 insertions(+), 75 deletions(-) delete mode 100644 packages/browser-wallet/test/Web3ProofRequest.test.ts diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e8977fbc8..5867c6efc 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0.2", + "version": "1.1.0", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts index 33752ddd0..2da97ad1a 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts @@ -26,7 +26,6 @@ const da: typeof en = { residence: 'Zero Knowledge bevis for bopælsland', idDocType: 'Zero Knowledge bevis for identitetsdokumenttype', idDocIssuer: 'Zero Knowledge bevis for identitetsdokumentudsteder', - secret: 'Zero Knowledge bevis' }, names: { age: 'Alder', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts index 7cdaf90a1..1d0ec3d0e 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts @@ -24,7 +24,6 @@ export default { residence: 'Zero Knowledge proof of country of residence', idDocType: 'Zero Knowledge proof of identity document type', idDocIssuer: 'Zero Knowledge proof of identity document issuer', - secret: 'Zero Knowledge proof' }, names: { age: 'Age', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 8ffe86372..6ee45694c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -14,6 +14,7 @@ import { VerifiableCredentialSchema, VerifiableCredentialStatus, } from '@shared/storage/types'; +import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,7 +26,6 @@ import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, - getVerifiableCredentialPublicKeyfromSubjectDID, getViableWeb3IdCredentialsForStatement, SecretStatementV2, } from './utils'; @@ -178,7 +178,7 @@ export default function DisplayWeb3Statement({ if (!verifiableCredentials) { return []; } - return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials); + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); }, [credentialStatement.idQualifier.issuers]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); @@ -201,7 +201,7 @@ export default function DisplayWeb3Statement({ return verifiableCredentialSchemas.value[schemaId]; } return null; - }, [chosenCredential?.id, verifiableCredentials?.length]); + }, [chosenCredential?.id, verifiableCredentialSchemas.loading]); const metadata = useCredentialMetadata(chosenCredential); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 1eadd1d8e..648681e69 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -97,7 +97,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }); const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials || []) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value || []) ); const request = { @@ -120,11 +120,11 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentialSchemas.loading || identities.loading) { + if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading) { return null; } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index f41924510..566d4b3ee 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -9,7 +9,6 @@ import { Network, AtomicStatementV2, RevealStatementV2, - ContractAddress, CommitmentInput, createWeb3IdDID, StatementTypes, @@ -17,6 +16,11 @@ import { import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; import { ClassName } from 'wallet-common-helpers'; +import { + getContractAddressFromIssuerDID, + getCredentialIdFromSubjectDID, + getVerifiableCredentialPublicKeyfromSubjectDID, +} from '@shared/utils/verifiable-credential-helpers'; export type SecretStatementV2 = Exclude; @@ -27,44 +31,6 @@ export interface DisplayCredentialStatementProps extends ClassName { net: Network; } -/** Takes a Web3IdCredential issuer DID string and returns the contract address - * @param did a issuer DID string on the form: "did:ccd:testnet:sci:INDEX:SUBINDEX/issuer" - * @returns the contract address INDEX;SUBINDEX - */ -export function getContractAddressFromIssuerDID(did: string): ContractAddress { - const split = did.split(':'); - if (split.length !== 6 || split[3] !== 'sci') { - throw new Error('Given DID did not follow expected format'); - } - const index = BigInt(split[4]); - const subindex = BigInt(split[5].substring(0, split[5].indexOf('/'))); - return { index, subindex }; -} - -/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential - * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" - * @returns the public key KEY - */ -export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { - const split = did.split('/'); - if (split.length !== 3 || split[0].split(':')[3] !== 'sci') { - throw new Error(`Given DID did not follow expected format:${did}`); - } - return split[2]; -} - -/** Takes a AccountCredential subject DID string and returns the credential id of the account credential - * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" - * @returns the credId CREDID - */ -export function getCredentialIdFromSubjectDID(did: string) { - const split = did.split(':'); - if (split.length !== 5 || split[3] !== 'cred') { - throw new Error(`Given DID did not follow expected format: ${did}`); - } - return split[4]; -} - /** * Build the commitmentInputs required to create a presentation for the given statement. */ diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 2482cb008..26776ae2c 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -717,3 +717,45 @@ export function getDIDNetwork(did: string): 'mainnet' | 'testnet' { } return network; } + +/** Takes a Web3IdCredential issuer DID string and returns the contract address + * @param did a issuer DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/issuer" + * @returns the contract address INDEX;SUBINDEX + */ +export function getContractAddressFromIssuerDID(did: string): ContractAddress { + const preSplit = did.split('/'); + if (preSplit[1] !== 'issuer') { + throw new Error('Given DID did not follow expected format'); + } + const split = preSplit[0].split(':'); + if ((split.length !== 6 && split.length !== 5) || split[split.length - 3] !== 'sci') { + throw new Error('Given DID did not follow expected format'); + } + const index = BigInt(split[split.length - 2]); + const subindex = BigInt(split[split.length - 1]); + return { index, subindex }; +} + +/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential + * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" + * @returns the public key KEY + */ +export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { + const split = did.split('/'); + if (split.length !== 3 || split[0].split(':')[split[0].split(':').length - 3] !== 'sci') { + throw new Error(`Given DID did not follow expected format:${did}`); + } + return split[2]; +} + +/** Takes a AccountCredential subject DID string and returns the credential id of the account credential + * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" + * @returns the credId CREDID + */ +export function getCredentialIdFromSubjectDID(did: string) { + const split = did.split(':'); + if ((split.length !== 5 && split.length !== 4) || split[split.length - 2] !== 'cred') { + throw new Error(`Given DID did not follow expected format: ${did}`); + } + return split[split.length - 1]; +} diff --git a/packages/browser-wallet/test/Web3ProofRequest.test.ts b/packages/browser-wallet/test/Web3ProofRequest.test.ts deleted file mode 100644 index 670e367e3..000000000 --- a/packages/browser-wallet/test/Web3ProofRequest.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - getCredentialIdFromSubjectDID, - getContractAddressFromIssuerDID, - getVerifiableCredentialPublicKeyfromSubjectDID, -} from '../src/popup/pages/Web3ProofRequest/utils'; - -test('getContractAddressFromIssuerDID', () => { - const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); - expect(address.index).toBe(1337n); - expect(address.subindex).toBe(42n); -}); - -test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { - const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( - 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' - ); - expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); -}); - -test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { - const credId = getCredentialIdFromSubjectDID( - 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' - ); - expect(credId).toBe( - 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' - ); -}); diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index 6dce34ce5..cf927efa0 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -4,6 +4,9 @@ import { getCredentialHolderId, getCredentialRegistryContractAddress, getPublicKeyfromPublicKeyIdentifierDID, + getCredentialIdFromSubjectDID, + getContractAddressFromIssuerDID, + getVerifiableCredentialPublicKeyfromSubjectDID, } from '../src/shared/utils/verifiable-credential-helpers'; import { mainnet, testnet } from '../src/shared/constants/networkConfiguration'; @@ -82,3 +85,47 @@ test('credential Id is created with correct network', () => { 'did:ccd:mainnet:sci:2:7/credentialEntry/4799ec95500850d9368ca012faf60e9d632d3b1768d608c7e5e3d53fe96d669a' ); }); + +test('getContractAddressFromIssuerDID extracts contract address', () => { + const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); + expect(address.index).toBe(1337n); + expect(address.subindex).toBe(42n); +}); + +test('getContractAddressFromIssuerDID extracts contract address without network', () => { + const address = getContractAddressFromIssuerDID('did:ccd:sci:1338:43/issuer'); + expect(address.index).toBe(1338n); + expect(address.subindex).toBe(43n); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID extracts public key', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID extracts public key without network', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getCredentialIdFromSubjectDID extracts credId', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); + +test('getCredentialIdFromSubjectDID extracts credId without network', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); From c574e6ef21ea5b726af18b5407bed235cbb591b9 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:50:20 +0200 Subject: [PATCH 08/19] Add missing documentation --- packages/browser-wallet-api-helpers/README.md | 15 +++++++++++++++ .../src/wallet-api-types.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index c74af76fa..7bf05869c 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -232,6 +232,21 @@ provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { }); ``` +### Request Verifiable Presentation for web3Id statements + +It is possible to request a verifiable presentation for a given set of web3Id statements. The function takes 2 arguments. A challenge to ensure that the proof was not generated for a different context, and the statements to be proven. This method returns a Promise resolving with the verifiable presentation for the statements. + +If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects proving the statement, the Promise will reject. + +The following exemplifies requesting a verifiable presentation for a statement named myIdStatement (To see how to create a statement check out our documentation) with a challenge of "12346789ABCD". + +```typescript +const statement = myIdStatement; +const challenge = '12346789ABCD'; +const provider = await detectConcordiumProvider(); +const verifiablePresentation = await provider.requestVerifiablePresentation(challenge, statement); +``` + ## Events ### Account changed diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index acc51e524..aa65240df 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -222,7 +222,10 @@ interface MainWalletApi { ): Promise; /** - * @TODO write this + fix return type + * Request that the user provides a proof for the given statements. + * @param challenge bytes chosen by the verifier. Should be HEX encoded. + * @param statement the web3Id statements that should be proven. + * @returns The presentation for the statements. */ requestVerifiablePresentation(challenge: string, statements: CredentialStatements): Promise; } From 2847dffaa17e71295b57be2ce9944f0340da50ba Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:53:40 +0200 Subject: [PATCH 09/19] Disable broken validation --- .../browser-wallet/src/background/web3Id.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index ce3f6af91..e1be7b932 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -2,7 +2,9 @@ import { CredentialStatements, getVerifiablePresentation, Web3IdProofInput, - createConcordiumClient, verifyWeb3IdCredentialSignature, isHex, verifyAtomicStatements + createConcordiumClient, + verifyWeb3IdCredentialSignature, + isHex, } from '@concordium/web-sdk'; import { sessionVerifiableCredentials, @@ -166,16 +168,19 @@ export const runIfValidWeb3IdProof: RunCondition }; } try { - const statements: CredentialStatements = parse(msg.payload.statements); - // TODO Fix second parameter when SDK is updated - // If a statement does not verify, an error is thrown. - statements.every((credStatement) => verifyAtomicStatements(credStatement.statement, undefined as any)); + const statements: CredentialStatements = parse(msg.payload.statements); + // TODO Enable when SDK is updated + // // If a statement does not verify, an error is thrown. + // statements.every((credStatement) => verifyAtomicStatements(credStatement.statement)); const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); if (!noEmptyQualifier) { return { run: false, - response: { success: false, message: `Statements must have at least 1 possible identity provider / issuer` }, + response: { + success: false, + message: `Statements must have at least 1 possible identity provider / issuer`, + }, }; } return { run: true }; From f09701fd69277efe3b2622ff599321d033164d2f Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 11:21:09 +0200 Subject: [PATCH 10/19] Refresh Statement component when paging --- .../src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 648681e69..0830c46c8 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -136,6 +136,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { dappName={dappName} credentialStatement={statements[currentStatementIndex]} net={net} + key={currentStatementIndex} setChosenId={(newId) => setIds((currentIds) => { const newIds = [...currentIds]; From fce454a154e3b74acda66c2a5d480419ad46d192 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 17:28:38 +0200 Subject: [PATCH 11/19] Address review comments --- examples/add-example-Web3Id/index.html | 56 +++++++++++---- .../browser-wallet/src/background/web3Id.ts | 3 - .../IdProofRequest/DisplayStatement/utils.ts | 7 +- .../VerifiableCredentialHooks.tsx | 2 +- .../Web3ProofRequest/AccountStatement.tsx | 17 +---- .../Web3ProofRequest/DisplayStatement.tsx | 3 +- .../VerifiableCredentialStatement.tsx | 15 ++-- .../Web3ProofRequest/Web3ProofRequest.tsx | 8 +-- .../popup/pages/Web3ProofRequest/i18n/da.ts | 41 +++++++++++ .../src/popup/pages/Web3ProofRequest/utils.ts | 71 ++++++++++++------- .../src/shared/utils/contract-helpers.ts | 9 ++- 11 files changed, 159 insertions(+), 73 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index e07a37497..57f22252f 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -10,25 +10,14 @@ let currentAccountAddress = ''; async function setupPage() { const provider = await concordiumHelpers.detectConcordiumProvider(); - document.getElementById('requestAccounts').addEventListener('click', () => { provider.requestAccounts().then((accountAddresses) => { currentAccountAddress = accountAddresses[0]; document.getElementById('accountAddress').innerHTML = currentAccountAddress; }); }); - provider.on('accountDisconnected', (accountAddress) => (currentAccountAddress = undefined)); - provider.on('accountChanged', (accountAddress) => (currentAccountAddress = accountAddress)); - provider.on('chainChanged', (chain) => alert(chain)); - document.getElementById('web3Proof').addEventListener('click', () => { - const statement = new concordiumSDK.Web3StatementBuilder() - .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => - b - .revealAttribute('degreeType') - .addRange('graduationDate', '2000-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) - ) - .getStatements(); + + function sendStatement(statement) { // Should be not be hardcoded const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; provider @@ -41,7 +30,41 @@ console.log(error); alert(error); }); + } + + provider.on('accountDisconnected', (accountAddress) => (currentAccountAddress = undefined)); + provider.on('accountChanged', (accountAddress) => (currentAccountAddress = accountAddress)); + provider.on('chainChanged', (chain) => alert(chain)); + // Request proofs + document.getElementById('web3ProofWeb3IdOnly').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => + b + .revealAttribute('degreeType') + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + ).getStatements(); + sendStatement(statement); + }); + document.getElementById('web3ProofIdOnly').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') + ).getStatements(); + sendStatement(statement); }); + document.getElementById('web3ProofMixed').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') + ) + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => + b + .revealAttribute('degreeType') + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + ).getStatements(); + sendStatement(statement); + }); + // Add credential document.getElementById('addWeb3Id').addEventListener('click', () => { const values = { degreeType: degreeType.value, @@ -131,6 +154,11 @@

    Attribute values:



    - +

    Request Proofs:

    + +
    + +
    + diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index e1be7b932..e6c540814 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -157,9 +157,6 @@ export const createWeb3IdProofHandler: ExtensionMessageHandler = (msg, _sender, return true; }; -/** - * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. - */ export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { if (!isHex(msg.payload.challenge)) { return { diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts index 00855d8a8..284f4e29d 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts @@ -253,8 +253,13 @@ export function getStatementDescription( return undefined; } -export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { +export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + + if (!identity) { + // TODO Should we write something here? + return ''; + } const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; if (!hasAttribute) { diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 92fc85db3..8d3058bf7 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -98,7 +98,7 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { const storedCredentialMetadata = storedMetadata.value[credentialEntry.credentialInfo.metadataUrl.url]; if (!storedCredentialMetadata) { throw new Error( - `Attempted to find credential metadata for credentialId: ${credential?.id} but none was found!` + `Attempted to find credential metadata for credentialId: ${credentialEntry.credentialInfo.credentialHolderId} but none was found!` ); } setMetadata(storedCredentialMetadata); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index a06898165..a8e205360 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -23,9 +23,9 @@ import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplaySt import CredentialSelector from './CredentialSelector'; import { DisplayCredentialStatementProps, getViableAccountCredentialsForStatement, SecretStatementV2 } from './utils'; import { - getStatementDescription, isoToCountryName, SecretStatement, + useStatementDescription, useStatementHeader, useStatementName, useStatementValue, @@ -37,21 +37,6 @@ type DisplaySecretStatementV2Props = ClassName & { statement: SecretStatementV2; }; -export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { - const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); - - if (!identity) { - // TODO Should we write something here? - return ''; - } - const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; - if (!hasAttribute) { - return t('missingAttribute', { identityName: identity.name }); - } - - return getStatementDescription(statement, t, i18n.resolvedLanguage); -} - export function DisplaySecretStatementV2({ dappName, statement, identity, className }: DisplaySecretStatementV2Props) { const v1Statement: SecretStatement = statement as SecretStatement; const header = useStatementHeader(v1Statement); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx index 3a6581802..015f42a0f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx @@ -18,5 +18,6 @@ export function DisplayCredentialStatement({ if (isVerifiableCredentialStatement(credentialStatement)) { return ; } - return null; + + throw new Error('Invalid Statement encountered'); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 6ee45694c..ae03a7b18 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -31,6 +31,7 @@ import { } from './utils'; function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { + // TODO use localization here const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; return property.title; } @@ -65,7 +66,7 @@ export function useStatementDescription(statement: SecretStatementV2, schema: Ve case StatementTypes.AttributeNotInSet: return t('nonMembership', { name, setNames: listToString(statement.set) }); default: - return undefined; + throw new Error('Unknown statement type encountered: ' + statement.type); } } @@ -117,9 +118,9 @@ export function DisplayWeb3RevealStatement({ const lines: StatementLine[] = statements.map((s) => { const { value } = attributes[s.attributeTag]; - const property = schema.properties.credentialSubject.properties.attributes.properties[s.attributeTag]; + const title = getPropertyTitle(s.attributeTag, schema); return { - attribute: property.title, + attribute: title, value: value.toString() ?? 'Unavailable', isRequirementMet: value !== undefined, }; @@ -137,12 +138,12 @@ export function DisplayWeb3SecretStatement({ const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const value = useStatementValue(statements, schema); const header = t('headers.secret'); - const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; + const title = getPropertyTitle(statements.attributeTag, schema); const description = useStatementDescription(statements, schema); const lines: StatementLine[] = [ { - attribute: property.title, + attribute: title, value, isRequirementMet: value !== undefined, }, @@ -175,11 +176,11 @@ export default function DisplayWeb3Statement({ const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const validCredentials = useMemo(() => { - if (!verifiableCredentials) { + if (verifiableCredentials.loading) { return []; } return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); - }, [credentialStatement.idQualifier.issuers]); + }, [verifiableCredentials.loading]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 0830c46c8..08d9a8259 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -80,8 +80,8 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { if (!network) { throw new Error('Network is not specified'); } - if (!ids.every((x) => Boolean(x))) { - throw new Error('Network is not specified'); + if (!canProve) { + throw new Error('The statements are not satisfied and cannot be proven'); } const global = await getGlobal(client); @@ -97,7 +97,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }); const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value || []) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value) ); const request = { @@ -120,7 +120,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading, canProve]); useEffect(() => onClose(onReject), [onClose, onReject]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts new file mode 100644 index 000000000..5a46362db --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -0,0 +1,41 @@ +import type en from './en'; + +const t: typeof en = { + header: '{{dappName}} anmoder om følgende information om dig:', + accept: 'Godkend', + reject: 'Afvis', + continue: 'Fortsæt', + displayStatement: { + requirementsMet: 'Du opfylder kravet', + requirementsNotMet: 'Du opfylder ikke kravet', + revealDescription: + '<1>Vigtigt: {{dappName}} får adgang til alt information der står ovenfor. Du skal kun acceptere at afsløre informationen, hvis du har tillid til servicen og hvis du er bekendt med deres privathedspolitik.', + revealTooltip: { + header: 'Afslører information <1 />', + body: 'Når du afslører information til en tredjepart, kan de beholde denne information. Dette betyder, at du kun bør afsløre information til dem hvis du er bekendt med deres databrugs- samt databeskyttelses-politik.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + }, + secretTooltip: { + header: 'Zero Knowledge beviser', + body: 'Zero Knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + }, + headers: { + reveal: 'Information der afsløres', + secret: 'Zero Knowledge bevis', + }, + proofs: { + range: 'Mellem {{ lower }} og {{ upper }}', + membership: '1 af de følgende', + nonMembership: 'Ingen af de følgende', + }, + descriptions: { + range: 'Dette vil bevise at deres {{ name }} er mellem {{ lower }} og {{ upper }}', + membership: 'Dette vil bevise at deres {{ name }} er en af følgende:\n{{ setNames }}', + nonMembership: 'This will prove that your {{ name }} er IKKE en af følgende:\n{{ setNames }}', + missingAttribute: 'Denne Attribut kan ikke findes på identiteten "{{identityName}}"', + } + }, + failedProof: 'Bevis kunne ikke oprettes', + failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 566d4b3ee..38b0a03c8 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -21,6 +21,7 @@ import { getCredentialIdFromSubjectDID, getVerifiableCredentialPublicKeyfromSubjectDID, } from '@shared/utils/verifiable-credential-helpers'; +import { areContractAddressesEqual } from '@shared/utils/contract-helpers'; export type SecretStatementV2 = Exclude; @@ -31,32 +32,12 @@ export interface DisplayCredentialStatementProps extends ClassName { net: Network; } -/** - * Build the commitmentInputs required to create a presentation for the given statement. - */ -export function getCommitmentInput( +function getAccountCredentialCommitmentInput( statement: RequestStatement, wallet: ConcordiumHdWallet, identities: ConfirmedIdentity[], credentials: WalletCredential[], - verifiableCredentials: VerifiableCredential[] -): CommitmentInput { - if (statement.type) { - const cred = verifiableCredentials?.find((c) => c.id === statement.id); - - if (!cred) { - throw new Error('IdQualifier not fulfilled'); - } - - return createWeb3CommitmentInputWithHdWallet( - wallet, - getContractAddressFromIssuerDID(cred.issuer), - cred.index, - cred.credentialSubject, - cred.randomness, - cred.signature - ); - } +) { const credId = getCredentialIdFromSubjectDID(statement.id); const credential = credentials.find((cred) => cred.credId === credId); @@ -75,11 +56,49 @@ export function getCommitmentInput( identity.providerIndex, identity.idObject.value.attributeList, wallet, - identity.providerIndex, + identity.index, credential.credNumber ); } +function createWeb3CommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + verifiableCredentials: VerifiableCredential[] +) { + const cred = verifiableCredentials?.find((c) => c.id === statement.id); + + if (!cred) { + throw new Error('IdQualifier not fulfilled'); + } + + return createWeb3CommitmentInputWithHdWallet( + wallet, + getContractAddressFromIssuerDID(cred.issuer), + cred.index, + cred.credentialSubject, + cred.randomness, + cred.signature + ); +} + +/** + * Build the commitmentInputs required to create a presentation for the given statement. + */ +export function getCommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + identities: ConfirmedIdentity[], + credentials: WalletCredential[], + verifiableCredentials: VerifiableCredential[] +): CommitmentInput { + // TODO replace with isVerifiableCredentialRequestStatement when SDK is updated + if (statement.type) { + return createWeb3CommitmentInput(statement, wallet, verifiableCredentials); + } + return getAccountCredentialCommitmentInput(statement, wallet, identities, credentials); +} + /** * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. @@ -93,7 +112,7 @@ export function getViableAccountCredentialsForStatement( return credentials?.filter((c) => { if (allowedIssuers.includes(c.providerIndex)) { const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); - if (identity && identity.status === CreationStatus.Confirmed) { + if (identity) { return canProveCredentialStatement(credentialStatement, identity.idObject.value.attributeList); } } @@ -101,6 +120,7 @@ export function getViableAccountCredentialsForStatement( }); } +// TODO Replace with canProveAtomicStatement when SDK is updated function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { const value = cred.credentialSubject.attributes[statement.attributeTag]; switch (statement.type) { @@ -127,8 +147,9 @@ export function getViableWeb3IdCredentialsForStatement( // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; const allowedCredentials = verifiableCredentials?.filter((vc) => - allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) + allowedContracts.some((address) => areContractAddressesEqual(address, getContractAddressFromIssuerDID(vc.issuer))) ); + return allowedCredentials.filter((cred) => credentialStatement.statement.every((stm) => doesCredentialSatisfyStatement(stm, cred)) ); diff --git a/packages/browser-wallet/src/shared/utils/contract-helpers.ts b/packages/browser-wallet/src/shared/utils/contract-helpers.ts index 031ef5432..456c6b173 100644 --- a/packages/browser-wallet/src/shared/utils/contract-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/contract-helpers.ts @@ -1,4 +1,4 @@ -import { InstanceInfo } from '@concordium/web-sdk'; +import { ContractAddress, InstanceInfo } from '@concordium/web-sdk'; /** * Get the name of a contract. @@ -9,3 +9,10 @@ import { InstanceInfo } from '@concordium/web-sdk'; export function getContractName(instanceInfo: InstanceInfo): string | undefined { return instanceInfo.name.substring(5); } + +/** + * Determine whether two contract addresses are the same + */ +export function areContractAddressesEqual(a: ContractAddress, b: ContractAddress) { + return a.index === b.index && a.subindex === b.subindex; +} From 51b1c1ed3fd21f08a1e6c8ff4b46861636e29925 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 17:31:14 +0200 Subject: [PATCH 12/19] Replace whitelist with allowlist in api-helpers README --- packages/browser-wallet-api-helpers/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index 7bf05869c..0d664705d 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -51,14 +51,14 @@ declare global { ### connect -To request a connection to the wallet from the user, the `connect` method has to be invoked. The method returns a `Promise` resolving with information related to the most recently selected account, which has whitelisted the dApp, or rejecting if the request is rejected in the wallet. If the wallet is locked, then this call prompts the user to first unlock the wallet before accepting or rejecting the connection request. +To request a connection to the wallet from the user, the `connect` method has to be invoked. The method returns a `Promise` resolving with information related to the most recently selected account, which has allowlisted the dApp, or rejecting if the request is rejected in the wallet. If the wallet is locked, then this call prompts the user to first unlock the wallet before accepting or rejecting the connection request. ```typescript const provider = await detectConcordiumProvider(); const accountAddress = await provider.connect(); ``` -N.B. In the current version, if the dApp is already whitelisted, but not by the currently selected account, the returned account will not actually be the most recently selected account, but instead the oldest account that has whitelisted the dApp. +N.B. In the current version, if the dApp is already allowlisted, but not by the currently selected account, the returned account will not actually be the most recently selected account, but instead the oldest account that has allowlisted the dApp. ### getMostRecentlySelectedAccount @@ -94,13 +94,13 @@ const provider = await detectConcordiumProvider(); const genesisHash = await provider.getSelectedChain(); ``` -N.B. In the current version, if the currently selected account has not whitelisted the dApp, the returned account will not actually be the most recently selected account, but instead the oldest account that has whitelisted the dApp. +N.B. In the current version, if the currently selected account has not allowlisted the dApp, the returned account will not actually be the most recently selected account, but instead the oldest account that has allowlisted the dApp. ### sendTransaction To send a transaction, three arguments need to be provided: The account address for the account in the wallet that should sign the transaction, a transaction type and a corresponding payload. Invoking `sendTransaction` returns a `Promise`, which resolves with the transaction hash for the submitted transaction. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the transaction, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the transaction, the `Promise` will reject. The following exemplifies how to create a simple transfer of funds from one account to another. Please note that [@concordium/web-sdk](https://github.com/Concordium/concordium-node-sdk-js/tree/main/packages/web) is used to provide the correct formats and types for the transaction payload. @@ -160,7 +160,7 @@ const parameterSchema = { It is possible to sign arbitrary messages using the keys for an account stored in the wallet, by invoking the `signMessage` method. The first parameter is the account to be used for signing the message. This method returns a `Promise` resolving with a signature of the message. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the meesage, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the meesage, the `Promise` will reject. The message should be either a utf8 string or an object with the following fields: @@ -206,7 +206,7 @@ In this example the user will be shown: It is possible to suggest CIS-2 tokens to be added to an account's display. This method returns a `Promise` resolving with a list containing the ids of the tokens that were added. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the meesage, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the meesage, the `Promise` will reject. The following exemplifies requesting tokens with id AA and BB from the contract on index 1399, and subindex 0 to the account `2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk`. @@ -236,7 +236,7 @@ provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { It is possible to request a verifiable presentation for a given set of web3Id statements. The function takes 2 arguments. A challenge to ensure that the proof was not generated for a different context, and the statements to be proven. This method returns a Promise resolving with the verifiable presentation for the statements. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects proving the statement, the Promise will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects proving the statement, the Promise will reject. The following exemplifies requesting a verifiable presentation for a statement named myIdStatement (To see how to create a statement check out our documentation) with a challenge of "12346789ABCD". @@ -286,7 +286,7 @@ provider.connect().then((accountAddress) => (selectedAccountAddress = accountAdd The wallet API exposes access to a JSON-RPC client. This allows a dApp to communicate with the same node as the wallet is connected to, and enables dApps to access the JSON-RPC interface without being connected to a separate server itself. The client is accessed as shown in the example below. The dApp does not need to recreate the client again when the wallet changes node or network, the client will always use the wallet's current connected JSON-RPC server. -If you have not connected with the wallet (or previously been whitelisted), the commands will not be executed and the method will throw an error. +If you have not connected with the wallet (or previously been allowlisted), the commands will not be executed and the method will throw an error. ```typescript const provider = await detectConcordiumProvider(); From eafc6db58cc5b965d368adee7dc7a4718fbd1898 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 16 Aug 2023 11:33:35 +0200 Subject: [PATCH 13/19] Only use active web3Id credentials for proofs --- .../VerifiableCredentialStatement.tsx | 5 +-- .../Web3ProofRequest/Web3ProofRequest.tsx | 31 +++++++++++++++++-- .../src/popup/pages/Web3ProofRequest/utils.ts | 22 +++++++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index ae03a7b18..660071401 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -66,7 +66,7 @@ export function useStatementDescription(statement: SecretStatementV2, schema: Ve case StatementTypes.AttributeNotInSet: return t('nonMembership', { name, setNames: listToString(statement.set) }); default: - throw new Error('Unknown statement type encountered: ' + statement.type); + throw new Error(`Unknown statement type encountered: ${statement.type}`); } } @@ -165,6 +165,7 @@ export default function DisplayWeb3Statement({ dappName, setChosenId, net, + statuses, }: DisplayCredentialStatementProps) { const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute @@ -179,7 +180,7 @@ export default function DisplayWeb3Statement({ if (verifiableCredentials.loading) { return []; } - return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value, statuses); }, [verifiableCredentials.loading]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 08d9a8259..24a6e5bdc 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -8,6 +8,7 @@ import { ConcordiumHdWallet, isAccountCredentialStatement, Web3IdProofInput, + ConcordiumGRPCClient, } from '@concordium/web-sdk'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; @@ -30,8 +31,11 @@ import { } from '@popup/store/verifiable-credential'; import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; import { parse } from '@shared/utils/payload-helpers'; -import { DisplayCredentialStatement } from './DisplayStatement'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; +import { getVerifiableCredentialStatus } from '@shared/utils/verifiable-credential-helpers'; +import { useAsyncMemo } from 'wallet-common-helpers'; import { getCommitmentInput } from './utils'; +import { DisplayCredentialStatement } from './DisplayStatement'; type Props = { onSubmit(presentationString: string): void; @@ -48,6 +52,18 @@ interface Location { }; } +async function getAllCredentialStatuses( + client: ConcordiumGRPCClient, + credentials: VerifiableCredential[] +): Promise> { + const statuses = await Promise.all( + credentials.map((credential) => + getVerifiableCredentialStatus(client, credential.id).then((status) => [credential.id, status]) + ) + ); + return Object.fromEntries(statuses); +} + export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; @@ -73,6 +89,16 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); + // TODO filter so that we only look up VC that are viable for some statement + const statuses = useAsyncMemo( + () => + verifiableCredentials.loading + ? Promise.resolve(undefined) + : getAllCredentialStatuses(client, verifiableCredentials.value), + undefined, + [verifiableCredentials.loading] + ); + const handleSubmit = useCallback(async () => { if (!recoveryPhrase) { throw new Error('Missing recovery phrase'); @@ -124,7 +150,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading) { + if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading || !statuses) { return null; } @@ -144,6 +170,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return newIds; }) } + statuses={statuses} /> + + +
    + + ); +} + export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; @@ -88,8 +112,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const credentials = useAtomValue(credentialsAtom); const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); - const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); - // TODO filter so that we only look up VC that are viable for some statement const statuses = useAsyncMemo( () => @@ -100,6 +122,26 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { [verifiableCredentials.loading] ); + const validCredentials = useMemo(() => { + if (identities.loading || verifiableCredentials.loading || !statuses) { + return undefined; + } + return statements.map((statement) => { + if (isAccountCredentialStatement(statement)) { + return getViableAccountCredentialsForStatement(statement, identities.value, credentials); + } + if (isVerifiableCredentialStatement(statement)) { + return getViableWeb3IdCredentialsForStatement(statement, verifiableCredentials.value, statuses); + } + throw new Error('Unknown statement type'); + }); + }, [identities.loading, verifiableCredentials.loading, Boolean(statuses)]); + + const canProve = useMemo( + () => validCredentials && validCredentials.every((x) => x.length > 0), + [Boolean(validCredentials)] + ); + const handleSubmit = useCallback(async () => { if (!recoveryPhrase) { throw new Error('Missing recovery phrase'); @@ -107,9 +149,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { if (!network) { throw new Error('Network is not specified'); } - if (!canProve) { - throw new Error('The statements are not satisfied and cannot be proven'); - } const global = await getGlobal(client); const wallet = ConcordiumHdWallet.fromHex(recoveryPhrase, net); @@ -157,20 +196,30 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading, canProve]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading || !statuses) { + if ( + verifiableCredentials.loading || + verifiableCredentialSchemas.loading || + identities.loading || + !validCredentials + ) { return null; } + if (!canProve) { + return ; + } + return (
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 5a46362db..9b030d361 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -32,10 +32,12 @@ const t: typeof en = { membership: 'Dette vil bevise at deres {{ name }} er en af følgende:\n{{ setNames }}', nonMembership: 'This will prove that your {{ name }} er IKKE en af følgende:\n{{ setNames }}', missingAttribute: 'Denne Attribut kan ikke findes på identiteten "{{identityName}}"', - } + }, }, failedProof: 'Bevis kunne ikke oprettes', failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', + unableToProve: + ' {{ dappName }} har anmodet et bevis for identitet fra dig, men du opfølger ikke kravene for beviset, så du kan ikke lave et bevis', }; export default t; From 4750a2701b1551b3cd243074333d7279826ff2b5 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 17 Aug 2023 14:58:07 +0200 Subject: [PATCH 19/19] Manually appease linter --- packages/browser-wallet/src/background/id-proof.ts | 7 +------ packages/browser-wallet/src/background/index.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index d8b9a4463..efeaa569c 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,9 +1,4 @@ -import { - getIdProof, - IdProofInput, - IdProofOutput, - verifyIdstatement, -} from '@concordium/web-sdk'; +import { getIdProof, IdProofInput, IdProofOutput, verifyIdstatement } from '@concordium/web-sdk'; import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { isHex } from 'wallet-common-helpers'; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 60421e4f3..6bf6592b6 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -39,7 +39,13 @@ import { setPopupSize, testPopupOpen, } from './window-management'; -import {runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof } from './web3Id'; +import { + runIfValidWeb3IdCredentialRequest, + web3IdAddCredentialFinishHandler, + createWeb3IdProofHandler, + runIfValidWeb3IdProof, +} from './web3Id'; + const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -252,7 +258,10 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); -bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), createWeb3IdProofHandler); +bgMessageHandler.handleMessage( + createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), + createWeb3IdProofHandler +); const NOT_WHITELISTED = 'Site is not whitelisted';