From 13da64653e17f32a0bfea6cca398c7c2db336a6e Mon Sep 17 00:00:00 2001 From: Hunain Bin Sajid Date: Wed, 7 Aug 2024 16:56:00 +0500 Subject: [PATCH 1/2] fix: remove trailing slash from agent and boot url (#186) --- src/components/ui/input/input.tsx | 1 + src/pages/background/services/config.ts | 11 +++++++++-- src/pages/background/services/session.ts | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/ui/input/input.tsx b/src/components/ui/input/input.tsx index 721fa774..693f003c 100644 --- a/src/components/ui/input/input.tsx +++ b/src/components/ui/input/input.tsx @@ -44,6 +44,7 @@ const StyledInputError = styled.p` color: ${({ theme }) => theme?.colors?.error}; font-size: 12px; margin: 0; + overflow-wrap: break-word; `; export const Input = ({ diff --git a/src/pages/background/services/config.ts b/src/pages/background/services/config.ts index 07aa04eb..af12b88c 100644 --- a/src/pages/background/services/config.ts +++ b/src/pages/background/services/config.ts @@ -1,5 +1,6 @@ import { browserStorageService } from "@pages/background/services/browser-storage"; import { default as defaultVendor } from "@src/config/vendor.json"; +import { removeSlash } from "@shared/utils"; const CONFIG_ENUMS = { VENDOR_URL: "vendor-url", @@ -63,7 +64,10 @@ const Config = () => { }; const setBootUrl = async (token: string) => { - await browserStorageService.setValue(CONFIG_ENUMS.BOOT_URL, token); + await browserStorageService.setValue( + CONFIG_ENUMS.BOOT_URL, + removeSlash(token) + ); }; const getBootUrl = async (): Promise => { @@ -73,7 +77,10 @@ const Config = () => { }; const setAgentUrl = async (token: string) => { - await browserStorageService.setValue(CONFIG_ENUMS.AGENT_URL, token); + await browserStorageService.setValue( + CONFIG_ENUMS.AGENT_URL, + removeSlash(token) + ); }; const getHasOnboarded = async () => { diff --git a/src/pages/background/services/session.ts b/src/pages/background/services/session.ts index 77f8854c..7f1ce02a 100644 --- a/src/pages/background/services/session.ts +++ b/src/pages/background/services/session.ts @@ -2,7 +2,7 @@ import { browserStorageService } from "@pages/background/services/browser-storag import { ObjectOfObject, ISession } from "@config/types"; const SESSION_ENUMS = { - EXPIRY_IN_MINS: 5, + EXPIRY_IN_MINS: 30, SESSIONS: "sessions", }; From c94aed1810e5ed0c1fac5d714ba04d8906e1eed8 Mon Sep 17 00:00:00 2001 From: Arshdeep Date: Tue, 27 Aug 2024 06:48:01 -0400 Subject: [PATCH 2/2] feat: implement create data attestation credential feat: - implement create data attestation credential - get credential by said, including cesr data - add utils functions to parse schema Signed-off-by: arshdeep singh --- example-web/my-app/.gitignore | 1 + package-lock.json | 2 +- src/config/event-types.ts | 3 + src/pages/background/handlers/index.ts | 5 + src/pages/background/handlers/resource.ts | 30 ++++ src/pages/background/services/signify.ts | 126 +++++++++++++-- src/pages/content/index.tsx | 76 ++++++++- src/pages/popup/constants.ts | 2 + src/shared/signify-utils.ts | 188 ++++++++++++++++++++++ 9 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 src/shared/signify-utils.ts diff --git a/example-web/my-app/.gitignore b/example-web/my-app/.gitignore index 4d29575d..8692cf66 100644 --- a/example-web/my-app/.gitignore +++ b/example-web/my-app/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/package-lock.json b/package-lock.json index cd3d44c5..e2a80a40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6106,7 +6106,7 @@ }, "node_modules/typescript": { "version": "4.9.5", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/config/event-types.ts b/src/config/event-types.ts index 2d52d0ef..070e48c0 100644 --- a/src/config/event-types.ts +++ b/src/config/event-types.ts @@ -22,6 +22,9 @@ export const CS_EVENTS = { fetch_resource_auto_signin_signature: `${CS}-${EVENT_TYPE.fetch_resource}-auto-signin-signature`, fetch_resource_signed_headers: `${CS}-${EVENT_TYPE.fetch_resource}-signed-headers`, fetch_resource_tab_signin: `${CS}-${EVENT_TYPE.fetch_resource}-tab-signin`, + fetch_resource_credential: `${CS}-${EVENT_TYPE.fetch_resource}-credential`, + + create_resource_data_attestation_credential: `${CS}-${EVENT_TYPE.create_resource}-data-attestation-credential`, vendor_info_get_vendor_data: `${CS}-${EVENT_TYPE.vendor_info}-get-vendor-data`, vendor_info_provide_config_url: `${CS}-${EVENT_TYPE.vendor_info}-provide-config-url`, diff --git a/src/pages/background/handlers/index.ts b/src/pages/background/handlers/index.ts index 1b097ea8..fb02616f 100644 --- a/src/pages/background/handlers/index.ts +++ b/src/pages/background/handlers/index.ts @@ -17,6 +17,8 @@ import { handleFetchSignins, handleFetchTabSignin, handleUpdateAutoSignin, + handleCreateAttestationCredential, + handleFetchCredential } from "./resource"; import { handleGetVendorData, handleAttemptSetVendorData } from "./vendorInfo"; import { @@ -75,6 +77,9 @@ export function initCSHandler() { handleFetchSignifyHeaders ); handler.set(CS_EVENTS.fetch_resource_tab_signin, handleFetchTabSignin); + handler.set(CS_EVENTS.fetch_resource_credential, handleFetchCredential); + + handler.set(CS_EVENTS.create_resource_data_attestation_credential, handleCreateAttestationCredential); handler.set(CS_EVENTS.vendor_info_get_vendor_data, handleGetVendorData); handler.set( diff --git a/src/pages/background/handlers/resource.ts b/src/pages/background/handlers/resource.ts index e51276af..aa707e47 100644 --- a/src/pages/background/handlers/resource.ts +++ b/src/pages/background/handlers/resource.ts @@ -126,6 +126,13 @@ export async function handleFetchCredentials({ sendResponse }: IHandler) { } } +export async function handleFetchCredential({ sendResponse, data }: IHandler) { + const cred = await signifyService.getCredential(data.id, data.includeCESR); + sendResponse({ + data: { credential: cred ?? null }, + }); +} + export async function handleCreateIdentifier({ sendResponse, data }: IHandler) { try { const resp = await signifyService.createAID(data.name); @@ -180,6 +187,29 @@ export async function handleCreateSignin({ sendResponse, data }: IHandler) { } } +export async function handleCreateAttestationCredential({ + sendResponse, + url, + tabId, + data, +}: IHandler) { + try { + const resp = await signifyService.createAttestationCredential({ + origin: getDomainFromUrl(url!), + credData: data.credData, + schemaSaid: data.schemaSaid, + tabId: tabId! + }); + sendResponse({ + data: { ...resp }, + }); + } catch (error: any) { + sendResponse({ + error: { code: 503, message: error?.message }, + }); + } +} + export async function handleUpdateAutoSignin({ sendResponse, data }: IHandler) { const resp = await signinResource.updateDomainAutoSignin(data?.signin); sendResponse({ diff --git a/src/pages/background/services/signify.ts b/src/pages/background/services/signify.ts index bfaf8942..62fb7ebc 100644 --- a/src/pages/background/services/signify.ts +++ b/src/pages/background/services/signify.ts @@ -1,11 +1,12 @@ import browser from "webextension-polyfill"; -import { SignifyClient, Tier, ready, randomPasscode } from "signify-ts"; +import { SignifyClient, Tier, ready, randomPasscode, Saider, IssueCredentialResult, CredentialData } from "signify-ts"; import { sendMessage } from "@src/shared/browser/runtime-utils"; import { userService } from "@pages/background/services/user"; import { configService } from "@pages/background/services/config"; import { sessionService } from "@pages/background/services/session"; import { IIdentifier, ISignin } from "@config/types"; import { SW_EVENTS } from "@config/event-types"; +import { formatAsCredentialEdgeOrRuleObject, getSchemaFieldOfEdge, parseSchemaEdgeOrRuleSection, setNodeValueInEdge, waitOperation } from "@src/shared/signify-utils"; const PASSCODE_TIMEOUT = 5; @@ -137,13 +138,9 @@ const Signify = () => { }; // credential identifier => credential.sad.d - const getCredentialWithCESR = async (credentialIdentifier: string) => { + const getCredential = async (credentialIdentifier: string, includeCESR: boolean = false) => { validateClient(); - try { - return await _client?.credentials().get(credentialIdentifier, true); - } catch (error) { - console.error(error); - } + return await _client?.credentials().get(credentialIdentifier, includeCESR); }; const disconnect = async () => { @@ -173,7 +170,7 @@ const Signify = () => { let credentialResp; if (signin.credential) { credentialResp = { raw: signin.credential, cesr: null }; - const cesr = await getCredentialWithCESR(signin.credential?.sad?.d); + const cesr = await getCredential(signin.credential?.sad?.d, true); credentialResp.cesr = cesr; } await sessionService.create({ tabId, origin, aidName: aidName! }); @@ -221,8 +218,10 @@ const Signify = () => { resetTimeoutAlarm(); console.log("sreq", sreq); let jsonHeaders: { [key: string]: string } = {}; - for (const pair of sreq?.headers?.entries()) { - jsonHeaders[pair[0]] = pair[1]; + if (sreq?.headers) { + for (const pair of sreq.headers.entries()) { + jsonHeaders[pair[0]] = pair[1]; + } } return { @@ -230,6 +229,95 @@ const Signify = () => { }; }; + /** + * Create a data attestation credential, it is an untargeted ACDC credential i.e. there is no issuee. + * + * @param origin - origin url from where request is being made -- required + * @param credData - credential data object containing the credential attributes -- required + * @param schemaSaid - SAID of the schema -- required + * @param signin - signin object containing identifier or credential -- required + * @returns Promise - returns a signed headers request object + */ + const createAttestationCredential = async ({ + origin, + credData, + schemaSaid, + tabId + }: { + origin: string; + credData: any, + schemaSaid: string, + tabId: number; + }): Promise => { + // in case the client is not connected, try to connect + const connected = await isConnected(); + // connected is false, it means the client session timed out or disconnected by user + if (!connected) { + validateClient(); + } + + const session = await sessionService.get({ tabId, origin }); + let { aid, registry, rules, edge } = await getCreateCredentialPrerequisites(session.aidName, schemaSaid); + if (isGroupAid(aid) === true) { + throw new Error(`Attestation credential issuance by multisig identifier ${session.aidName} is not supported yet!`); + } + + let credArgs: CredentialData = { + i: aid.prefix, + ri: registry.regk, + s: schemaSaid, + a: credData, + r: rules + ? Object.keys(rules).length > 0 + ? Saider.saidify({ d: '', ...rules })[1] + : undefined + : undefined, + e: edge + ? Object.keys(edge).length > 0 + ? Saider.saidify({ d: '', ...edge })[1] + : undefined + : undefined + } + console.log("create credential args: ", credArgs); + let credResult = await createCredential(session.aidName, credArgs) + if (credResult && _client) { + await waitOperation(_client, credResult.op) + } + + return credResult; + }; + + const getCreateCredentialPrerequisites = async (aidName: string, schemaSaid: string): + Promise<{ aid: any | undefined; schema: any; registry: any, rules: any, edge: any }> => { + const aid = await _client?.identifiers().get(aidName); + + let registries = await _client?.registries().list(aidName) + if (registries == undefined || registries.length === 0) { + throw new Error(`No credential registries found for the AID ${aidName}`); + } + + let schema = await _client?.schemas().get(schemaSaid) + if (!schema || schema?.title == '404 Not Found') { + throw new Error(`Schema not found!`); + } + + const edgeObject = parseSchemaEdgeOrRuleSection(schema.properties?.e) + let edge = formatAsCredentialEdgeOrRuleObject(edgeObject) + let edgeSchema = getSchemaFieldOfEdge(edge) + if (edge && edgeSchema) { + let filter = { '-s': edgeSchema, '-a-i': aid?.prefix } + let creds = await _client?.credentials().list({ filter: filter, limit: 50 }) + if (creds && creds?.length > 0) { + edge = setNodeValueInEdge(edge, creds[0]?.sad.d) + } + } + + let parsedRules = parseSchemaEdgeOrRuleSection(schema.properties?.r) + let rules = formatAsCredentialEdgeOrRuleObject(parsedRules) + + return { aid, schema, registry: registries[0], rules, edge }; + }; + const getControllerID = async (): Promise => { validateClient(); const controllerId = await userService.getControllerId(); @@ -242,18 +330,36 @@ const Signify = () => { return await res?.op(); }; + const createCredential = async ( + name: string, + args: CredentialData + ): Promise => { + const result = await _client?.credentials().issue(name, args) + return result + } + + const isGroupAid = (aid: any): boolean => { + return ( + aid.hasOwnProperty('group') && + typeof aid.group === 'object' && + aid.group !== null + ) + } + return { connect, isConnected, disconnect, listIdentifiers, listCredentials, + getCredential, createAID, generatePasscode, bootAndConnect, getControllerID, getSignedHeaders, authorizeSelectedSignin, + createAttestationCredential }; }; diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 00bf296b..8745c0f2 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -145,6 +145,76 @@ window.addEventListener( ); } + break; + case TAB_STATE.CREATE_DATA_ATTEST_CRED: + const { data: credData, error: attestCredError } = + await sendMessageWithExtId<{ + rurl?: string; + }>(getExtId(), { + type: CS_EVENTS.create_resource_data_attestation_credential, + data: event.data.payload, + }); + requestId = event?.data?.requestId ?? ""; + rurl = event?.data?.rurl ?? rurl; + + console.log("create attest credential resp data", credData); + if (attestCredError) { + window.postMessage( + { + type: "/signify/reply", + error: attestCredError?.message, + requestId, + rurl, + }, + "*" + ); + } else { + window.postMessage( + { + type: "/signify/reply", + payload: credData, + requestId, + rurl, + }, + "*" + ); + } + + break; + case TAB_STATE.GET_CREDENTIAL: + const { data: cred, error: credError } = + await sendMessageWithExtId<{ + rurl?: string; + }>(getExtId(), { + type: CS_EVENTS.fetch_resource_credential, + data: event.data.payload, + }); + requestId = event?.data?.requestId ?? ""; + rurl = event?.data?.rurl ?? rurl; + + console.log("get credential result", cred); + if (credError) { + window.postMessage( + { + type: "/signify/reply", + error: credError?.message, + requestId, + rurl, + }, + "*" + ); + } else { + window.postMessage( + { + type: "/signify/reply", + payload: cred, + requestId, + rurl, + }, + "*" + ); + } + break; default: break; @@ -163,9 +233,9 @@ browser.runtime.onMessage.addListener(async function ( if (sender.id === getExtId()) { console.log( "Content script received message from browser extension: " + - message.type + - ":" + - message.subtype + message.type + + ":" + + message.subtype ); if (message.type === "tab" && message.subtype === "reload-state") { if (getTabState() !== TAB_STATE.NONE) { diff --git a/src/pages/popup/constants.ts b/src/pages/popup/constants.ts index 935debc6..206b4fe2 100644 --- a/src/pages/popup/constants.ts +++ b/src/pages/popup/constants.ts @@ -6,5 +6,7 @@ export const TAB_STATE = { SIGN_REQUEST: "/signify/sign-request", CONFIGURE_VENDOR: "/signify/configure-vendor", SELECT_AUTO_SIGNIN: "select-auto-signin", + CREATE_DATA_ATTEST_CRED: "/signify/credential/create/data-attestation", + GET_CREDENTIAL: "/signify/credential/get", NONE: "none" } \ No newline at end of file diff --git a/src/shared/signify-utils.ts b/src/shared/signify-utils.ts new file mode 100644 index 00000000..4abe0e29 --- /dev/null +++ b/src/shared/signify-utils.ts @@ -0,0 +1,188 @@ +import { Operation, SignifyClient } from 'signify-ts' + +/** + * Wait for long running keria operation to become completed + * + * Use polling to check the status of the operation every second till max retries are over + * @async + * @param {SignifyClient} client Signify Client object + * @param {Operation} [op] long running keria operation + * @param {number} [maxRetries] Number of max retries, after which error will be thrown. Defaults to 30 + * @param {number} [delay] Delay in ms between retries, Defaults to 1000 + * + */ +export async function waitOperation( + client: SignifyClient, + op: Operation, + maxRetries: number = 30, + delay: number = 1000 +): Promise> { + const start = Date.now() + while (maxRetries-- > 0) { + op = await client.operations().get(op.name) + if (op.done === true) return op + + await new Promise((resolve) => setTimeout(resolve, delay)) + } + + throw new Error( + `Operation ${op.name} timed out after ${Date.now() - start}ms` + ) +} + +export function parseSchemaEdgeOrRuleSection(schemaSection: any): any { + if (!schemaSection) { + return null + } + const findObjectInOneOf = (oneOfArray: any[]) => { + return oneOfArray.find((item: any) => item.type === 'object') + } + + const parseProperties = (properties: any) => { + let parsedObject: any = {} + + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + let field = properties[key] + + // Handle oneOf inside the properties + if (field.oneOf) { + const objectInOneOf = findObjectInOneOf(field.oneOf) + if (objectInOneOf) { + // Recursive call to handle nested properties within oneOf + parsedObject[key] = parseProperties(objectInOneOf.properties) + } else { + parsedObject[key] = field.oneOf[0] + } + } else if (field.type === 'object') { + parsedObject[key] = parseProperties(field.properties) + } else { + parsedObject[key] = { + description: field.description, + type: field.type, + ...(field.const && { const: field.const }) + } + } + } + } + + return parsedObject + } + + if (schemaSection?.oneOf) { + const properties = schemaSection?.oneOf?.find( + (item: any) => item.type === 'object' + )?.properties + + if (properties) { + return parseProperties(properties) + } + } else if (schemaSection?.properties) { + const properties = schemaSection?.properties + return parseProperties(properties) + } else { + const properties = schemaSection?.properties + if (properties) { + return parseProperties(properties) + } + if ( + schemaSection && + typeof schemaSection === 'object' && + schemaSection.type === 'string' + ) { + return schemaSection.hasOwnProperty('const') + ? schemaSection.const + : undefined + } + } + + return undefined +} + +export const formatAsCredentialEdgeOrRuleObject = ( + schemaSectionObject: any +) => { + if ( + !schemaSectionObject || + (typeof schemaSectionObject === 'object' && + Object.keys(schemaSectionObject).length === 0) + ) { + return null + } + + const output: any = {} + + const parseNestedSchemaFieldObject = (nestedObject: any) => { + const nestedOutput: any = {} + + for (const key in nestedObject) { + if (nestedObject.hasOwnProperty(key)) { + const subItem = nestedObject[key] + + if (subItem && typeof subItem === 'object') { + if ('type' in subItem) { + nestedOutput[key] = parseSchemaFieldObject(subItem) + } else { + nestedOutput[key] = parseNestedSchemaFieldObject(subItem) + } + } + } + } + + return nestedOutput + } + + if (typeof schemaSectionObject === 'string' && schemaSectionObject) { + return schemaSectionObject + } else { + for (const key in schemaSectionObject) { + if (schemaSectionObject.hasOwnProperty(key)) { + const fieldItem = schemaSectionObject[key] + if (fieldItem && typeof fieldItem === 'object') { + if ('type' in fieldItem) { + output[key] = parseSchemaFieldObject(fieldItem) + } else { + output[key] = parseNestedSchemaFieldObject(fieldItem) + } + } + } + } + } + + return output +} + +const parseSchemaFieldObject = (item: any) => { + if (item && typeof item === 'object' && item.type === 'string') { + return item.hasOwnProperty('const') ? item.const : '' + } + return {} +} + +export const getSchemaFieldOfEdge = (edgeObj: any) => { + if (!edgeObj) { + return null + } + + for (const key in edgeObj) { + if (edgeObj.hasOwnProperty(key)) { + if (edgeObj[key].hasOwnProperty('s')) { + return edgeObj[key]['s'] + } + } + } + return null +} + +export const setNodeValueInEdge = (edgeObj: any, nodeSaid: string) => { + if (!edgeObj) { + return null + } + + for (const key in edgeObj) { + if (edgeObj[key].hasOwnProperty('n')) { + edgeObj[key]['n'] = nodeSaid + } + } + return edgeObj +}