From 8480f01a217123ed51a51fc235609a41174a4223 Mon Sep 17 00:00:00 2001 From: Edd <77212889+SinanovicEdis@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:07:35 +0100 Subject: [PATCH] feat: integrate sd-jwt (#657) Co-authored-by: martines3000 --- .changeset/seven-tables-serve.md | 8 + libs/extended-verification/package.json | 2 + .../src/Verification.service.ts | 5 + .../src/createVeramoAgent.ts | 4 +- packages/connector/src/snap.ts | 29 +- packages/dapp/package.json | 2 + .../[id]/CredentialPanel.tsx | 38 ++- .../share-presentation/[id]/SDJwtView.tsx | 149 ++++++++++ .../(public)/share-presentation/[id]/page.tsx | 28 +- .../[id]/templates/SdJwt.tsx | 191 ++++++++++++ .../src/app/api/share/presentation/route.ts | 24 +- .../CreateCredentialDisplay/index.tsx | 5 + .../CreatePresentationDisplay/index.tsx | 50 +++- .../SelectedVCsTableRow.tsx | 186 ++++++++---- .../SelectedVcShareTableRow.tsx | 165 +++++++++++ .../components/ShareCredentialModal/index.tsx | 110 ++++++- packages/dapp/src/messages/en.json | 18 +- packages/dapp/src/utils/format.ts | 26 ++ packages/dapp/src/utils/selectProofFormat.ts | 2 + packages/snap/package.json | 4 + packages/snap/post-process.js | 36 +++ packages/snap/snap.manifest.json | 2 +- packages/snap/src/SDJwt.service.ts | 106 +++++++ packages/snap/src/Signer.service.ts | 5 +- packages/snap/src/Snap.service.ts | 62 +++- packages/snap/src/UI.service.tsx | 22 +- packages/snap/src/index.ts | 3 + packages/snap/src/storage/Storage.service.ts | 11 +- packages/snap/src/utils/config.ts | 1 + packages/snap/src/utils/sign.ts | 2 +- packages/snap/src/utils/stateMigration.ts | 16 + packages/snap/src/veramo/Veramo.service.ts | 273 +++++++++++++++++- .../snap/tests/data/legacyStates/index.ts | 1 + .../tests/data/legacyStates/legacyStateV4.ts | 114 ++++++++ .../snap/tests/e2e/changePermission.spec.ts | 13 +- .../e2e/createVerifiableCredential.spec.ts | 19 +- .../e2e/createVerifiablePresentation.spec.ts | 19 +- .../snap/tests/e2e/importStateBackup.spec.ts | 82 +++++- packages/snap/tests/e2e/verifyData.spec.ts | 11 +- packages/snap/tests/unit/ceramic.spec.ts | 2 +- .../snap/tests/unit/requestParams.spec.ts | 93 ++++++ packages/snap/vite.config.mts | 2 +- packages/types/package.json | 1 + packages/types/src/api.ts | 5 + packages/types/src/constants.ts | 3 +- packages/types/src/legacy/index.ts | 1 + packages/types/src/legacy/stateV4.ts | 124 ++++++++ packages/types/src/methods.ts | 6 + packages/types/src/params.ts | 36 ++- packages/types/src/requests.ts | 2 + pnpm-lock.yaml | 196 ++++++++++++- 51 files changed, 2170 insertions(+), 145 deletions(-) create mode 100644 .changeset/seven-tables-serve.md create mode 100644 packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/SDJwtView.tsx create mode 100644 packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/SdJwt.tsx create mode 100644 packages/dapp/src/components/ShareCredentialModal/SelectedVcShareTableRow.tsx create mode 100644 packages/snap/src/SDJwt.service.ts create mode 100644 packages/snap/tests/data/legacyStates/legacyStateV4.ts create mode 100644 packages/types/src/legacy/stateV4.ts diff --git a/.changeset/seven-tables-serve.md b/.changeset/seven-tables-serve.md new file mode 100644 index 000000000..46d313ac8 --- /dev/null +++ b/.changeset/seven-tables-serve.md @@ -0,0 +1,8 @@ +--- +"@blockchain-lab-um/masca-connector": minor +"@blockchain-lab-um/dapp": minor +"@blockchain-lab-um/masca": minor +"@blockchain-lab-um/masca-types": minor +--- + +Added SD-JWT support diff --git a/libs/extended-verification/package.json b/libs/extended-verification/package.json index 50dbe6170..03380da10 100644 --- a/libs/extended-verification/package.json +++ b/libs/extended-verification/package.json @@ -33,6 +33,8 @@ "@blockchain-lab-um/did-provider-key": "1.1.0-beta.1", "@blockchain-lab-um/masca-types": "1.4.0-beta.1", "@blockchain-lab-um/utils": "1.4.0-beta.1", + "@sd-jwt/crypto-nodejs": "^0.7.2", + "@sd-jwt/sd-jwt-vc": "^0.8.0", "@veramo/core": "6.0.0", "@veramo/credential-eip712": "6.0.0", "@veramo/credential-status": "6.0.0", diff --git a/libs/extended-verification/src/Verification.service.ts b/libs/extended-verification/src/Verification.service.ts index 1e3170baa..518ec77a7 100644 --- a/libs/extended-verification/src/Verification.service.ts +++ b/libs/extended-verification/src/Verification.service.ts @@ -53,6 +53,11 @@ export class VerificationService { VerificationService.veramoAgent = await createVeramoAgent(); } + // TODO: Implement this (edis) + static async verifySdJwtPresentation(args: any): Promise { + throw new Error('Not implemented'); + } + static async verify( data: W3CVerifiablePresentation | W3CVerifiableCredential, options?: { ebsiChecks?: boolean } diff --git a/libs/extended-verification/src/createVeramoAgent.ts b/libs/extended-verification/src/createVeramoAgent.ts index d225d65da..e4a975b49 100644 --- a/libs/extended-verification/src/createVeramoAgent.ts +++ b/libs/extended-verification/src/createVeramoAgent.ts @@ -20,7 +20,9 @@ export interface CreateVeramoAgentProps { providers?: Record; } -export const createVeramoAgent = async (props?: CreateVeramoAgentProps) => { +export const createVeramoAgent = async ( + props?: CreateVeramoAgentProps +): Promise> => { const { providers } = props ?? {}; // This any is here, because Veramo does't export the `ProviderConfiguration` type // from `ethr-did-resolver` and `ens-did-resolver` package uses Ethers v5 still with a diff --git a/packages/connector/src/snap.ts b/packages/connector/src/snap.ts index dc3b1083f..0e886780f 100644 --- a/packages/connector/src/snap.ts +++ b/packages/connector/src/snap.ts @@ -3,6 +3,7 @@ import type { AvailableMethods, CreateCredentialRequestParams, CreatePresentationRequestParams, + DecodeSdJwtPresentationRequestParams, DeleteCredentialsOptions, HandleAuthorizationRequestParams, HandleCredentialOfferRequestParams, @@ -15,6 +16,7 @@ import type { QueryCredentialsRequestResult, SaveCredentialOptions, SaveCredentialRequestResult, + SdJwtCredential, SetCurrentAccountRequestParams, SignDataRequestParams, VerifyDataRequestParams, @@ -115,6 +117,27 @@ async function createPresentation( return signedResult; } +/** + * Decode a SD-JWT presentation + * @param params - parameters for decoding a SD-JWT presentation + * @return Result - decoded SD-JWT presentation + */ +async function decodeSdJwtPresentation( + this: Masca, + params: DecodeSdJwtPresentationRequestParams +): Promise> { + return sendSnapMethod( + this, + { + method: 'decodeSdJwtPresentation', + params: { + ...params, + }, + }, + this.snapId + ); +} + /** * Save a VC in Masca under the currently selected MetaMask account * @param vc - VC to be saved @@ -404,11 +427,12 @@ async function createCredential( const vcResult = result as Result; - if (isError(vcResult)) { + // Fix (no issuer id): the sd-jwt has iss value and not issuer with id value + if (vcResult.success && vcResult.data.proofType === 'sd-jwt') { return vcResult; } - if (vcResult.data.proof) { + if (isError(vcResult)) { return vcResult; } @@ -637,6 +661,7 @@ export class Masca { saveCredential: wrapper(saveCredential.bind(this)), queryCredentials: wrapper(queryCredentials.bind(this)), createPresentation: wrapper(createPresentation.bind(this)), + decodeSdJwtPresentation: wrapper(decodeSdJwtPresentation.bind(this)), togglePopups: wrapper(togglePopups.bind(this)), addTrustedDapp: wrapper(addTrustedDapp.bind(this)), removeTrustedDapp: wrapper(removeTrustedDapp.bind(this)), diff --git a/packages/dapp/package.json b/packages/dapp/package.json index b8c320059..1653ee5b0 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -32,12 +32,14 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", "@react-oauth/google": "^0.12.1", + "@sd-jwt/crypto-browser": "^0.7.2", "@supabase/supabase-js": "^2.43.1", "@tanstack/react-query": "^5.35.1", "@tanstack/react-table": "^8.16.0", "@types/dompurify": "^3.0.5", "@types/js-cookie": "^3.0.6", "@types/jsdom": "^21.1.6", + "@types/uuid": "^9.0.8", "@veramo/core": "6.0.0", "@veramo/credential-eip712": "6.0.0", "@veramo/credential-w3c": "6.0.0", diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx index 61765c331..80a8ec267 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx @@ -15,14 +15,17 @@ import { getFirstWord } from '@/utils/format'; import { convertTypes } from '@/utils/string'; import { Normal } from './templates/Normal'; import { EduCTX } from './templates/EduCTX'; +import { SdJwt } from './templates/SdJwt'; +import type { SdJwtCredential } from '@blockchain-lab-um/masca-connector'; interface FormattedPanelProps { - credential: VerifiableCredential; + credential: VerifiableCredential | SdJwtCredential; } enum Templates { Normal = 0, EduCTX = 1, + SdJwt = 2, } const CredentialPanel = ({ credential }: FormattedPanelProps) => { @@ -32,13 +35,20 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => { const router = useRouter(); // Local state - const types = useMemo(() => convertTypes(credential.type), [credential.type]); + const types = useMemo(() => { + // Check if the credential is an sd-jwt type + if (Object.keys(credential).includes('_sd_alg')) { + return convertTypes((credential as SdJwtCredential).vct); + } + return convertTypes(credential.type as string | string[]); + }, [credential]); + const [jsonModalOpen, setJsonModalOpen] = useState(false); const [selectedJsonData, setSelectedJsonData] = useState({}); const isValid = useMemo(() => { if (!credential.expirationDate) return true; - return Date.parse(credential.expirationDate) > Date.now(); + return Date.parse(credential.expirationDate as string) > Date.now(); }, [credential]); const selectJsonData = (data: any) => { @@ -51,6 +61,10 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => { ? credential.type : [credential.type]; + if (Object.keys(credential).includes('_sd_alg')) { + return Templates.SdJwt; + } + if (credentialTypes.includes('EducationCredential')) { return Templates.EduCTX; } @@ -63,7 +77,7 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => { case Templates.EduCTX: return ( { }} /> ); + case Templates.SdJwt: + return ( + + ); default: return ( { + const t = useTranslations('SDJwtView'); + const router = useRouter(); + const pathname = usePathname(); + + const [verificationInfoModalOpen, setVerificationInfoModalOpen] = + useState(false); + + const isValid = true; + const holder = credential.sub ?? ''; + const issuanceDate = credential.iat ? credential.iat * 1000 : null; + + return ( + <> +
+ setVerificationInfoModalOpen(true)} + className="absolute right-3 top-3 h-6 w-6 cursor-pointer" + /> +
+
+
+
+

+ {t('holder')} +

+

+
+ + + {formatDid(holder)} + + + ar + +
+

+
+ {issuanceDate && ( +
+

+ {t('presented')} +

+ {new Date(issuanceDate).toDateString()} +
+ )} +
+

+ {t('credentials')} +

+
+
+
+
+ + {isValid ? ( + + ) : ( + + )} + +
+

+ {t('credential-status')} +

+

+ {isValid ? t('valid') : t('invalid')} +

+
+
+
+
+
+ { + const params = new URLSearchParams(window.location.search); + params.set('page', val.toString()); + params.set('view', 'Normal'); + router.replace(`${pathname}?${params.toString()}`); + }} + classNames={{ + wrapper: + 'space-x-2 pl-4 pr-6 pb-2 pt-1 rounded-none rounded-b-2xl bg-gradient-to-tr from-pink-100 to-orange-100 dark:from-navy-blue-700 dark:to-navy-blue-700', + item: 'flex-nowrap w-5 h-5 text-black dark:text-navy-blue-200 bg-inherit shadow-none active:bg-inherit active:text-black dark:active:text-navy-blue-200 [&[data-hover=true]:not([data-active=true])]:bg-inherit', + cursor: + 'w-5 h-5 rounded-full dark:bg-orange-accent-dark bg-pink-500 text-white dark:text-navy-blue-800', + }} + /> +
+ +
+
+ + + ); +}; diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx index c3f21e7d5..f7bb35280 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx @@ -9,6 +9,7 @@ import { usePresentation, useUpdatePresentationViews } from '@/hooks'; import { NormalViewButton } from './NormalViewButton'; import { VerificationService } from '@blockchain-lab-um/extended-verification'; import type { VerifiablePresentation } from '@veramo/core'; +import { SDJwtView } from './SDJwtView'; export const revalidate = 0; @@ -17,6 +18,13 @@ const verifyPresentation = async (presentation: VerifiablePresentation) => { return VerificationService.verify(presentation); }; +const verifySdJwtPresentation = async (presentation: string) => { + await VerificationService.init(); + return VerificationService.verifySdJwtPresentation({ + presentation: presentation, + }); +}; + export default async function Page({ params: { id }, searchParams, @@ -35,10 +43,17 @@ export default async function Page({ await useUpdatePresentationViews(id); - const verificationResult = await verifyPresentation(data.presentation); - const { presentation } = data; + const isSdJwtPresentation = + Array.isArray(presentation) && + Object.keys(presentation[0]).includes('_sd_alg'); + + // TODO: Implement sd-jwt verification + const verificationResult = isSdJwtPresentation + ? null + : await verifyPresentation(data.presentation); + const credentials = presentation.verifiableCredential ? presentation.verifiableCredential.map(decodeCredentialToObject) : []; @@ -48,7 +63,14 @@ export default async function Page({ return (
- {view === 'Normal' && ( + {view === 'Normal' && isSdJwtPresentation && ( + + )} + {view === 'Normal' && !isSdJwtPresentation && verificationResult && ( ; + viewJsonText: string; + selectJsonData: React.Dispatch>; +}) => ( + <> + {Object.entries(data).map(([key, value]: [string, any]) => { + if (value === null || value === '') return null; + return ( + + {(() => { + if (key === 'id') { + return ( + <> +
+
+ +
+
+ + ); + } + + if (key === 'address') return ; + if (key === 'image') return ; + + const isObject = typeof value === 'object'; + const formattedKey = key.replace(/([A-Z])/g, ' $1').trim(); + return ( +
+

+ {formattedKey}: +

+
+ {isObject ? ( + + ) : ( + value + )} +
+
+ ); + })()} +
+ ); + })} + +); + +type SdJwtProps = { + credential: SdJwtCredential; + title: { + subject: string; + issuer: string; + dates: string; + disclosures: string; + }; + viewJsonText: string; + selectJsonData: (data: any) => void; +}; + +const DisclosureDetails = ({ + data, + viewJsonText, + selectJsonData, +}: { + data: Record; + viewJsonText: string; + selectJsonData: React.Dispatch>; +}) => ( + <> + {Object.entries(data).map(([key, value]: [string, any]) => { + if (value === null || value === '') return null; + + const isObject = typeof value === 'object'; + + return ( + +
+

+ {value.key}: +

+

+ {value.value} +

+
+ {isObject ? ( + + ) : ( + value + )} +
+
+
+ ); + })} + +); + +export const SdJwt = ({ + credential, + title, + viewJsonText, + selectJsonData, +}: SdJwtProps) => { + return ( +
+
+
+

+ {title.subject} +

+ +
+ {credential.disclosures && credential.disclosures.length > 0 && ( +
+

+ {title.disclosures} +

+ +
+ )} +
+
+
+
+

+ {title.issuer} +

+
+
+ +
+
+
+
+

+ {title.dates} +

+ +
+
+
+
+ ); +}; diff --git a/packages/dapp/src/app/api/share/presentation/route.ts b/packages/dapp/src/app/api/share/presentation/route.ts index 6c3c8fcac..e8b159496 100644 --- a/packages/dapp/src/app/api/share/presentation/route.ts +++ b/packages/dapp/src/app/api/share/presentation/route.ts @@ -59,11 +59,27 @@ export async function POST(request: NextRequest) { }); } + const isSdJwtPresentation = + Array.isArray(presentation) && + Object.keys(presentation[0]).includes('_sd_alg'); + await VerificationService.init(); - const verifiedResult: Result = - await VerificationService.verify( - presentation as W3CVerifiablePresentation - ); + + // TODO: Implement sd-jwt verification + const verifiedResult: Result = isSdJwtPresentation + ? { + success: true, + data: { + verified: true, + details: { + credentials: [], + presentation: null, + }, + }, + } + : await VerificationService.verify( + presentation as W3CVerifiablePresentation + ); if (isError(verifiedResult)) { return new NextResponse('Failed to verify presentation', { diff --git a/packages/dapp/src/components/CreateCredentialDisplay/index.tsx b/packages/dapp/src/components/CreateCredentialDisplay/index.tsx index 294b963a0..d74a23ace 100644 --- a/packages/dapp/src/components/CreateCredentialDisplay/index.tsx +++ b/packages/dapp/src/components/CreateCredentialDisplay/index.tsx @@ -27,6 +27,7 @@ import { capitalizeString } from '@/utils/format'; const proofFormats: Record = { JWT: 'jwt', + 'SD-JWT': 'sd-jwt', 'JSON-LD': 'lds', EIP712Signature: 'EthereumEip712Signature2021', }; @@ -60,6 +61,7 @@ const CreateCredentialDisplay = () => { const [save, setSave] = useState(false); const [availableProofFormats, setAvailableProofFormats] = useState([ 'JWT', + 'SD-JWT', 'JSON-LD', 'EIP712Signature', ]); @@ -76,6 +78,9 @@ const CreateCredentialDisplay = () => { ) { setAvailableProofFormats(['EIP712Signature']); setFormat('EIP712Signature'); + } else if (didMethod === 'did:jwk') { + setAvailableProofFormats(['JWT', 'SD-JWT', 'JSON-LD', 'EIP712Signature']); + setFormat('SD-JWT'); } else { setAvailableProofFormats(['JWT', 'JSON-LD', 'EIP712Signature']); setFormat('JWT'); diff --git a/packages/dapp/src/components/CreatePresentationDisplay/index.tsx b/packages/dapp/src/components/CreatePresentationDisplay/index.tsx index f1a24c0c4..b04683f02 100644 --- a/packages/dapp/src/components/CreatePresentationDisplay/index.tsx +++ b/packages/dapp/src/components/CreatePresentationDisplay/index.tsx @@ -6,10 +6,7 @@ import { isError, } from '@blockchain-lab-um/masca-connector'; import { ArrowLeftIcon } from '@heroicons/react/20/solid'; -import type { - W3CVerifiableCredential, - W3CVerifiablePresentation, -} from '@veramo/core'; +import type { W3CVerifiablePresentation } from '@veramo/core'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -27,6 +24,7 @@ import { removeCredentialSubjectFilterString } from '@/utils/format'; const proofFormats: Record = { JWT: 'jwt', + 'SD-JWT': 'sd-jwt', 'JSON-LD': 'lds', EIP712Signature: 'EthereumEip712Signature2021', }; @@ -46,8 +44,12 @@ const CreatePresentationDisplay = () => { const [domain, setDomain] = useState(''); const [isInvalidMethod, setInvalidMethod] = useState(false); const [includesPolygonVC, setIncludesPolygonVC] = useState(false); + const [selectedSdJwtDisclosures, setSelectedSdJwtDisclosures] = useState< + string[] + >([]); const [availableProofFormats, setAvailableProofFormats] = useState([ 'JWT', + 'SD-JWT', 'JSON-LD', 'EIP712Signature', ]); @@ -76,6 +78,9 @@ const CreatePresentationDisplay = () => { ) { setAvailableProofFormats(['EIP712Signature']); setFormat('EIP712Signature'); + } else if (didMethod === 'did:jwk') { + setAvailableProofFormats(['JWT', 'SD-JWT', 'JSON-LD', 'EIP712Signature']); + setFormat('SD-JWT'); } else { setAvailableProofFormats(['JWT', 'JSON-LD', 'EIP712Signature']); setFormat('JWT'); @@ -97,14 +102,27 @@ const CreatePresentationDisplay = () => { (vc: QueryCredentialsRequestResult) => vc.metadata.id !== id ) ); + + setSelectedSdJwtDisclosures((prevSelectedDisclosures) => + prevSelectedDisclosures.filter( + (disclosure) => !disclosure.startsWith(`${id}/`) + ) + ); }; const handleCreatePresentation = async () => { if (!api) return; setLoading(true); - const vcs: W3CVerifiableCredential[] = selectedCredentials.map( - (vc) => removeCredentialSubjectFilterString(vc).data - ); + + const vcs = + format === 'SD-JWT' + ? selectedCredentials.map((vc) => ({ + id: vc.metadata.id, + encodedVc: vc.data.encoded, + })) + : selectedCredentials.map( + (vc) => removeCredentialSubjectFilterString(vc).data + ); const proofOptions = { type: '', domain, challenge }; @@ -112,6 +130,7 @@ const CreatePresentationDisplay = () => { vcs, proofFormat: proofFormats[format], proofOptions, + presentationFrame: selectedSdJwtDisclosures, }); if (isError(res)) { console.error(res); @@ -123,6 +142,21 @@ const CreatePresentationDisplay = () => { setVpModalOpen(true); setLoading(false); }; + + const handleDisclosureCheck = (id: string, key: string, checked: boolean) => { + const disclosureString = `${id}/${key}`; + if (checked) { + setSelectedSdJwtDisclosures([ + ...selectedSdJwtDisclosures, + disclosureString, + ]); + } else { + setSelectedSdJwtDisclosures( + selectedSdJwtDisclosures.filter((d) => d !== disclosureString) + ); + } + }; + return ( <>
@@ -156,7 +190,9 @@ const CreatePresentationDisplay = () => { {selectedCredentials.map((vc) => ( diff --git a/packages/dapp/src/components/SelectedVCsTableRow/SelectedVCsTableRow.tsx b/packages/dapp/src/components/SelectedVCsTableRow/SelectedVCsTableRow.tsx index a3fae1dae..98cc688ff 100644 --- a/packages/dapp/src/components/SelectedVCsTableRow/SelectedVCsTableRow.tsx +++ b/packages/dapp/src/components/SelectedVCsTableRow/SelectedVCsTableRow.tsx @@ -15,11 +15,20 @@ import { formatDid } from '@/utils/format'; interface SelectedVCsTableRowProps { vc: QueryCredentialsRequestResult; + selectedSdJwtDisclosures: string[]; handleRemove: (id: string) => void; + handleDisclosureCheck: ( + vcId: string, + disclosureKey: string, + checked: boolean + ) => void; } + const SelectedVCsTableRow = ({ vc, + selectedSdJwtDisclosures, handleRemove, + handleDisclosureCheck, }: SelectedVCsTableRowProps) => { const t = useTranslations('SelectedVCsTableRow'); @@ -45,77 +54,128 @@ const SelectedVCsTableRow = ({ validity = Date.now() < Date.parse(vc.data.expirationDate); return ( - - - - - - - - - {type} - - { - - - {formatDid(issuer)} - - - } - - - {!isPolygonVC(vc) ? ( + <> + + - - {validity === true ? ( - - ) : ( - - )} - + + - ) : ( - + + {type} + + { - + + {formatDid(issuer)} + + } + + + {!isPolygonVC(vc) ? ( + + + {validity === true ? ( + + ) : ( + + )} + + + ) : ( + + + + + + )} + + + + - )} - - - - - - - + + + {vc.data.disclosures?.length > 0 && ( + + + +
+ {vc.data.disclosures.map((disclosure) => ( +
+ handleDisclosureCheck( + vc.metadata.id, + disclosure.key, + !selectedSdJwtDisclosures.includes( + `${vc.metadata.id}/${disclosure.key}` + ) + ) + } + > + + handleDisclosureCheck( + vc.metadata.id, + disclosure.key, + e.target.checked + ) + } + className="form-checkbox h-4 w-4 text-blue-600" + /> + +
+ ))} +
+ + + )} + ); }; diff --git a/packages/dapp/src/components/ShareCredentialModal/SelectedVcShareTableRow.tsx b/packages/dapp/src/components/ShareCredentialModal/SelectedVcShareTableRow.tsx new file mode 100644 index 000000000..736c33a49 --- /dev/null +++ b/packages/dapp/src/components/ShareCredentialModal/SelectedVcShareTableRow.tsx @@ -0,0 +1,165 @@ +import type { + Disclosure, + QueryCredentialsRequestResult, +} from '@blockchain-lab-um/masca-connector'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + XCircleIcon, +} from '@heroicons/react/24/solid'; +import { Tooltip } from '@nextui-org/react'; +import { useTranslations } from 'next-intl'; + +import { isPolygonVC } from '@/utils/credential'; +import { formatDid } from '@/utils/format'; + +interface SelectedVcShareTableRowProps { + vc: QueryCredentialsRequestResult; + selectedSdJwtDisclosures: string[]; + handleDisclosureCheck: ( + vcId: string, + disclosureKey: string, + checked: boolean + ) => void; +} + +const SelectedVcShareTableRow = ({ + vc, + selectedSdJwtDisclosures, + handleDisclosureCheck, +}: SelectedVcShareTableRowProps) => { + const t = useTranslations('SelectedVCsTableRow'); + + let issuer = ''; + if (typeof vc.data.issuer === 'string') { + issuer = vc.data.issuer; + } else { + issuer = vc.data.issuer.id; + } + + let type = ''; + if (vc.data.type) { + if (typeof vc.data.type === 'string') { + type = vc.data.type; + } + if (Array.isArray(vc.data.type) && vc.data.type.length > 0) { + type = vc.data.type[vc.data.type.length - 1]; + } + } + + let validity = true; + if (vc.data.expirationDate) + validity = Date.now() < Date.parse(vc.data.expirationDate); + + return ( + <> + + {type} + + { + + + {formatDid(issuer)} + + + } + + + {!isPolygonVC(vc) ? ( + + + {validity === true ? ( + + ) : ( + + )} + + + ) : ( + + + + + + )} + + {vc.data.disclosures?.length > 0 && ( + + + +
+ {vc.data.disclosures.map((disclosure: Disclosure) => ( +
+ handleDisclosureCheck( + vc.metadata.id, + disclosure.key, + !selectedSdJwtDisclosures.includes( + `${vc.metadata.id}/${disclosure.key}` + ) + ) + } + > + + handleDisclosureCheck( + vc.metadata.id, + disclosure.key, + e.target.checked + ) + } + className="form-checkbox h-4 w-4 text-blue-600" + /> + +
+ ))} +
+ + + )} + + + ); +}; + +export default SelectedVcShareTableRow; diff --git a/packages/dapp/src/components/ShareCredentialModal/index.tsx b/packages/dapp/src/components/ShareCredentialModal/index.tsx index 071ba5649..ace2a178d 100644 --- a/packages/dapp/src/components/ShareCredentialModal/index.tsx +++ b/packages/dapp/src/components/ShareCredentialModal/index.tsx @@ -1,6 +1,8 @@ import { + type QueryCredentialsRequestResult, type Result, ResultObject, + type SdJwtCredential, isError, } from '@blockchain-lab-um/masca-connector'; import { @@ -23,6 +25,7 @@ import { TwitterIcon, TwitterShareButton, } from 'react-share'; +import { v4 as uuidv4 } from 'uuid'; import { useMascaStore, @@ -33,6 +36,7 @@ import { import { selectProofFormat } from '@/utils/selectProofFormat'; import { convertTypes } from '@/utils/string'; import Button from '../Button'; +import SelectedVcShareTableRow from './SelectedVcShareTableRow'; export const ShareCredentialModal = () => { const t = useTranslations('ShareCredentialModal'); @@ -57,11 +61,20 @@ export const ShareCredentialModal = () => { const [isLoading, setIsLoading] = useState(false); const [title, setTitle] = useState(''); + const [selectedCredentials, setSelectedCredentials] = + useState(); + const [selectedSdJwtDisclosures, setSelectedSdJwtDisclosures] = useState< + string[] + >([]); + const types = useMemo( () => credentials.map((credential) => ({ - key: credential.id, - value: convertTypes(credential.type).split(',')[0], + data: credential, + metadata: { + id: credential.id || uuidv4(), + type: convertTypes(credential.type).split(',')[0], + }, })), [credentials] ); @@ -94,18 +107,56 @@ export const ShareCredentialModal = () => { return true; }, [credentials]); - const stringifiedTypes = useMemo(() => JSON.stringify(types), [types]); + const stringifiedTypes = useMemo( + () => JSON.stringify(types.map((type) => type.metadata.type)), + [types] + ); // Functions const handleShareCredential = async () => { if (!api || !didMethod) return; setIsLoading(true); - let createPresentationResult: Result; + + const selectedProofFormat = selectProofFormat(didMethod); + + let createPresentationResult: Result< + VerifiablePresentation | SdJwtCredential[] + >; + + const isSdJwt = selectedProofFormat === 'sd-jwt' && didMethod === 'did:jwk'; + let vcs: { id: string; encodedVc: string }[] = []; + if (isSdJwt) { + // Preprocess credentials for creating SD-JWT presentation + vcs = credentials.map((credential, index) => ({ + id: types[index].metadata.id, + encodedVc: credential.encoded, + })); + } + try { createPresentationResult = await api.createPresentation({ - vcs: credentials, - proofFormat: selectProofFormat(didMethod), + vcs: isSdJwt ? vcs : credentials, + proofFormat: selectedProofFormat, + presentationFrame: selectedSdJwtDisclosures, }); + + if ( + selectedProofFormat === 'sd-jwt' && + createPresentationResult.success + ) { + let presentationsArray: string[] = []; + + if ('presentations' in createPresentationResult.data) { + presentationsArray = createPresentationResult.data.presentations.map( + (presentation: { presentation: string }) => + presentation.presentation + ); + } + + createPresentationResult = await api.decodeSdJwtPresentation({ + presentation: presentationsArray, + }); + } } catch (e: any) { console.log(e); createPresentationResult = ResultObject.error(e.message); @@ -182,10 +233,34 @@ export const ShareCredentialModal = () => { } }; + const handleDisclosureCheck = (id: string, key: string, checked: boolean) => { + const disclosureString = `${id}/${key}`; + if (checked) { + setSelectedSdJwtDisclosures([ + ...selectedSdJwtDisclosures, + disclosureString, + ]); + } else { + setSelectedSdJwtDisclosures( + selectedSdJwtDisclosures.filter((d) => d !== disclosureString) + ); + } + }; + useEffect(() => { setShareLink(null); }, [stringifiedTypes]); + useEffect(() => { + if (isOpen) { + const initialSelectedCredentials = types.map((type) => ({ + data: type.data, + metadata: type.metadata, + })); + setSelectedCredentials(initialSelectedCredentials); + } + }, [isOpen, types]); + return ( { placeholder={t('placeholder')} />
-
-

+
+
{t('selected')} -

-
- {types.map((type) => ( -
{type.value}
- ))}
+ + + {selectedCredentials?.map((vc) => ( + + ))} + +
diff --git a/packages/dapp/src/messages/en.json b/packages/dapp/src/messages/en.json index d22404772..95695a57a 100644 --- a/packages/dapp/src/messages/en.json +++ b/packages/dapp/src/messages/en.json @@ -285,6 +285,21 @@ "presentation-invalid": "Presentation is invalid", "verify-failed": "Failed to verify presentation" }, + "SDJwtView": { + "holder": "Holder", + "presented": "Presented", + "credentials": "Credentials", + "credential-status": "Status", + "credential-valid": "Credential is valid", + "credential-invalid": "Credential is invalid", + "valid": "Valid", + "invalid": "Invalid", + "presentation-status": "Status", + "presentation-valid": "Presentation is valid", + "presentation-invalid": "Presentation is invalid", + "verify-failed": "Failed to verify presentation", + "disclosures": "Disclosures" + }, "CredentialPanel": { "title": "Credential", "status": "Status", @@ -294,7 +309,8 @@ "subject": "SUBJECT", "view-json": "View JSON", "credential-valid": "Credential is valid", - "credential-invalid": "Credential is invalid" + "credential-invalid": "Credential is invalid", + "disclosures": "DISCLOSURES" }, "Error": { "title": "Something went wrong", diff --git a/packages/dapp/src/utils/format.ts b/packages/dapp/src/utils/format.ts index be59dc7ff..b5b7c90d0 100644 --- a/packages/dapp/src/utils/format.ts +++ b/packages/dapp/src/utils/format.ts @@ -20,6 +20,32 @@ export const stringifyCredentialSubject = ( ): QueryCredentialsRequestResult => { const verifiableCredential = queryCredentialsRequestResult.data; const { credentialSubject } = verifiableCredential; + + if (verifiableCredential?.proofType === 'sd-jwt') { + const modifiedQueryCredentialsRequestResult = { + ...queryCredentialsRequestResult, + data: { + ...verifiableCredential.credential, + credentialSubject: { + ...verifiableCredential.credential.credentialSubject, + filterString: JSON.stringify( + verifiableCredential.credential.credentialSubject + ), + } as CredentialSubject, + issuanceDate: new Date( + verifiableCredential.credential.iat * 1000 + ).toISOString(), + issuer: { id: verifiableCredential.credential.iss }, + type: verifiableCredential.credential.vct.split(','), + }, + }; + + return { + ...modifiedQueryCredentialsRequestResult, + metadata: queryCredentialsRequestResult.metadata, + }; + } + const modifiedQueryCredentialsRequestResult = { ...queryCredentialsRequestResult, data: { diff --git a/packages/dapp/src/utils/selectProofFormat.ts b/packages/dapp/src/utils/selectProofFormat.ts index 2bb41c10d..47f566c45 100644 --- a/packages/dapp/src/utils/selectProofFormat.ts +++ b/packages/dapp/src/utils/selectProofFormat.ts @@ -1,5 +1,7 @@ export const selectProofFormat = (method: string) => { switch (method) { + case 'did:jwk': + return 'sd-jwt'; case 'did:ethr': case 'did:pkh': case 'did:ens': diff --git a/packages/snap/package.json b/packages/snap/package.json index ea6f74f9c..3063c5b67 100644 --- a/packages/snap/package.json +++ b/packages/snap/package.json @@ -71,6 +71,10 @@ "@metamask/providers": "17.1.2", "@metamask/snaps-sdk": "6.9.0", "@metamask/utils": "9.3.0", + "@sd-jwt/core": "^0.7.2", + "@sd-jwt/crypto-nodejs": "^0.7.2", + "@sd-jwt/sd-jwt-vc": "^0.8.0", + "@sd-jwt/types": "^0.7.2", "@sphereon/pex": "3.3.3", "@veramo/core": "6.0.0", "@veramo/credential-eip712": "6.0.0", diff --git a/packages/snap/post-process.js b/packages/snap/post-process.js index cabc8a2af..d86056546 100644 --- a/packages/snap/post-process.js +++ b/packages/snap/post-process.js @@ -39,6 +39,42 @@ bundleString = bundleString.replaceAll( 'singleThread: true' ); +// [sd-jwt-veramo] - @sphereon/ssi-sdk-ext.did-utils +bundleString = bundleString.replaceAll( + 'global.navigator.userAgent.indexOf("Edge/") > -1', + 'false' +); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible +bundleString = bundleString.replaceAll( + 'window.crypto.createHash = require_hash7()(window.crypto);', + '' +); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible +bundleString = bundleString.replaceAll('var rf2 = require_browser21();', ''); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible +bundleString = bundleString.replaceAll( + 'window.crypto.randomFill = rf2.randomFill;', + '' +); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible +bundleString = bundleString.replaceAll( + 'window.crypto.randomFillSync = rf2.randomFillSync;', + '' +); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible (lib/extended-verification) +bundleString = bundleString.replaceAll( + 'window.crypto.createHash = require_hash6()(window.crypto);', + '' +); + +// [sd-jwt veramo plugin] - fix cannot add property createHash, object is not extensible (lib/extended-verification) +bundleString = bundleString.replaceAll('var rf2 = require_browser20();', ''); + console.log('[End]: Custom transform'); fs.writeFileSync(bundlePath, bundleString); diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 351b7b7de..ad7b6b18f 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -26,7 +26,7 @@ "./files/circuits/credentialAtomicQuerySigV2/circuit_final.zkey", "./files/circuits/credentialAtomicQuerySigV2/verification_key.json" ], - "shasum": "JodjqtKp5wf+vGLtAv3BWt+r8rflH91hTOkJBq4U+WU=" + "shasum": "7AzAYDZrCnI/TUi2oQgISevznktGYCkFgkcx9THLhIs=" }, "initialPermissions": { "endowment:ethereum-provider": {}, diff --git a/packages/snap/src/SDJwt.service.ts b/packages/snap/src/SDJwt.service.ts new file mode 100644 index 000000000..252a8036e --- /dev/null +++ b/packages/snap/src/SDJwt.service.ts @@ -0,0 +1,106 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import WalletService from './Wallet.service'; +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { ec as EC } from 'elliptic'; +import { bytesToBase64url, decodeBase64url } from '@veramo/utils'; + +type SdJwtPayload = Record; + +class SDJwtService { + static signer: (data: string) => Promise; + static verifier: ( + data: string, + signatureBase64Url: string + ) => Promise; + static instance: SDJwtInstance; + + /** + * Initializes the SDJwtService. + * + * This method sets up the SDJwtService by retrieving the wallet from the WalletService, + * extracting the private key, and creating the key pair. It also sets up the signer and + * verifier functions for the SDJwtService instance. + * + * @throws {Error} If the wallet cannot be retrieved or keys are missing. + * + * @returns {Promise} A promise that resolves when the initialization is complete. + */ + static async init(): Promise { + try { + const wallet = WalletService.get(); + + if (!wallet) { + // We can return here, becuase if the wallet is not set, we know the current method is not supported + return; + } + + const privateKeyHex = wallet.privateKey.slice(2); // Remove '0x' prefix + + const ec = new EC('p256'); + const keyPair = ec.keyFromPrivate(privateKeyHex, 'hex'); + const publicKey = keyPair.getPublic(); + + if (!keyPair || !publicKey) { + throw new Error('Keys are missing from WalletService'); + } + + SDJwtService.signer = async (data: string): Promise => { + const signature = keyPair.sign(Buffer.from(data)); + + return bytesToBase64url( + Buffer.concat([ + signature.r.toArrayLike(Buffer, 'be', 32), + signature.s.toArrayLike(Buffer, 'be', 32), + ]) + ); + }; + + SDJwtService.verifier = async ( + data: string, + signatureBase64Url: string + ): Promise => { + const signatureBuffer = decodeBase64url(signatureBase64Url); + + const r = signatureBuffer.substring(0, 32); + const s = signatureBuffer.substring(32); + + return keyPair.verify(Buffer.from(data), { r, s }); + }; + + SDJwtService.instance = new SDJwtInstance({ + signer: SDJwtService.signer + ? SDJwtService.signer + : async () => { + throw new Error('Signer not initialized'); + }, + verifier: SDJwtService.verifier + ? SDJwtService.verifier + : async () => { + throw new Error('Verifier not initialized'); + }, + signAlg: 'ES256', + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + kbSigner: SDJwtService.signer, + kbVerifier: SDJwtService.verifier, + kbSignAlg: 'ES256', + }); + } catch (e) { + console.error('Failed to initialize SDJwtService', e); + } + } + + /** + * Get the global SDJwtInstance + * @returns SDJwtInstance + */ + static get(): SDJwtInstance { + if (!SDJwtService.instance) { + throw new Error('---> SDJwtService is not initialized'); + } + return SDJwtService.instance; + } +} + +export default SDJwtService; diff --git a/packages/snap/src/Signer.service.ts b/packages/snap/src/Signer.service.ts index d062a232c..e03aae18f 100644 --- a/packages/snap/src/Signer.service.ts +++ b/packages/snap/src/Signer.service.ts @@ -78,7 +78,10 @@ class SignerService { const curve = method === 'did:key:jwk_jcs-pub' ? 'p256' : 'secp256k1'; const ctx = new EC(curve); - const ecPrivateKey = ctx.keyFromPrivate(wallet.privateKey.slice(2)); + const ecPrivateKey = ctx.keyFromPrivate( + wallet.privateKey.slice(2), + 'hex' + ); const alg = curve === 'secp256k1' ? 'ES256K' : 'ES256'; diff --git a/packages/snap/src/Snap.service.ts b/packages/snap/src/Snap.service.ts index 59ad4fe48..084648ade 100644 --- a/packages/snap/src/Snap.service.ts +++ b/packages/snap/src/Snap.service.ts @@ -3,11 +3,13 @@ import { CURRENT_STATE_VERSION, type CreateCredentialRequestParams, type CreatePresentationRequestParams, + type DecodeSdJwtPresentationRequestParams, type DeleteCredentialsRequestParams, type HandleAuthorizationRequestParams, type HandleCredentialOfferRequestParams, type QueryCredentialsRequestParams, type QueryCredentialsRequestResult, + type SdJwtCredential, type SaveCredentialRequestParams, type SaveCredentialRequestResult, type VerifyDataRequestParams, @@ -172,11 +174,39 @@ class SnapService { return unsignedVc; } - let storeString = ''; - if (save === true) { - storeString = `Data store(s): **${ - typeof store === 'string' ? store : store.join(', ') - }**`; + if (proofFormat === 'sd-jwt') { + // Hide the type value from the credential subject + const disclosureFrame = { + _sd: ['type'], + }; + + const vc = await VeramoService.createCredentialSdJwt({ + credential: minimalUnsignedCredential, + disclosureFrame, + }); + + const identifier = await VeramoService.getIdentifier(); + + const { did } = identifier; + + if ( + await UIService.createCredentialDialog({ + save, + store, + minimalUnsignedCredential: vc, + did, + }) + ) { + if (save === true) { + await VeramoService.saveCredential({ + verifiableCredential: vc, + store, + }); + } + return vc; + } + + throw new Error('User rejected create credential request'); } const vc = await VeramoService.createCredential({ @@ -191,7 +221,7 @@ class SnapService { if ( await UIService.createCredentialDialog({ save, - storeString, + store, minimalUnsignedCredential: vc, did, }) @@ -278,7 +308,12 @@ class SnapService { static async createPresentation( params: CreatePresentationRequestParams ): Promise { - const { vcs, proofFormat = 'jwt', proofOptions } = params; + const { + vcs, + proofFormat = 'jwt', + proofOptions, + presentationFrame = [], + } = params; const state = StorageService.get(); const method = state[CURRENT_STATE_VERSION].accountState[ @@ -293,7 +328,7 @@ class SnapService { } const unsignedVp = await VeramoService.createUnsignedPresentation({ - credentials: vcs, + credentials: vcs as W3CVerifiableCredential[], }); return unsignedVp; @@ -308,6 +343,7 @@ class SnapService { vcs, proofFormat, proofOptions, + presentationFrame, }); return res; @@ -316,6 +352,13 @@ class SnapService { throw new Error('User rejected create presentation request.'); } + static async decodeSdJwtPresentation( + params: DecodeSdJwtPresentationRequestParams + ): Promise { + const res = VeramoService.decodeSdJwtPresentation(params); + return res; + } + /** * Function that verifies data. * @param params.credential - VC to verify. @@ -518,6 +561,9 @@ class SnapService { await VeramoService.importIdentifier(); res = await SnapService.createPresentation(params); return ResultObject.success(res); + case 'decodeSdJwtPresentation': + res = await SnapService.decodeSdJwtPresentation(params); + return ResultObject.success(res); case 'deleteCredential': isValidDeleteCredentialsRequest( params, diff --git a/packages/snap/src/UI.service.tsx b/packages/snap/src/UI.service.tsx index 7aa063ad8..562e631fa 100644 --- a/packages/snap/src/UI.service.tsx +++ b/packages/snap/src/UI.service.tsx @@ -194,11 +194,11 @@ class UIService { static async createCredentialDialog(params: { save: boolean | undefined; - storeString: string; + store: string | string[]; minimalUnsignedCredential: any; did: string; }) { - const { save, storeString, minimalUnsignedCredential, did } = params; + const { save, store, minimalUnsignedCredential, did } = params; const uiPanelContent = ( @@ -213,7 +213,10 @@ class UIService { credential below? - {storeString} + + Data store(s):{' '} + {typeof store === 'string' ? store : store.join(', ')} + Credential: @@ -256,7 +259,7 @@ class UIService { } static async createPresentationDialog(params: { - vcs: W3CVerifiableCredential[]; + vcs: W3CVerifiableCredential[] | Array<{ id: string; encodedVc: string }>; did: string; }) { const { vcs, did } = params; @@ -274,9 +277,14 @@ class UIService { Credentials: - {vcs.map((vc) => ( - - ))} + {Array.isArray(vcs) + ? vcs.map((vc) => ( + + )) + : []} ); diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 1acf91ffe..89ef01ad5 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -11,6 +11,7 @@ import UIService from './UI.service'; import WalletService from './Wallet.service'; import StorageService from './storage/Storage.service'; import VeramoService from './veramo/Veramo.service'; +import SDJwtService from './SDJwt.service'; export const onRpcRequest: OnRpcRequestHandler = async ({ request, @@ -33,6 +34,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ await WalletService.init(); + await SDJwtService.init(); + await VeramoService.init(); await EthereumService.init(); diff --git a/packages/snap/src/storage/Storage.service.ts b/packages/snap/src/storage/Storage.service.ts index 03d089b61..b940b3865 100644 --- a/packages/snap/src/storage/Storage.service.ts +++ b/packages/snap/src/storage/Storage.service.ts @@ -6,7 +6,12 @@ import { import { getInitialSnapState } from '../utils/config'; import SnapStorage from './Snap.storage'; -import { migrateToV2, migrateToV3, migrateToV4 } from '../utils/stateMigration'; +import { + migrateToV2, + migrateToV3, + migrateToV4, + migrateToV5, +} from '../utils/stateMigration'; class StorageService { static instance: MascaState; @@ -59,6 +64,10 @@ class StorageService { newState = migrateToV4(newState); } + if (newState.v4) { + newState = migrateToV5(newState); + } + return newState; }; } diff --git a/packages/snap/src/utils/config.ts b/packages/snap/src/utils/config.ts index 921af8f77..50cd9c68c 100644 --- a/packages/snap/src/utils/config.ts +++ b/packages/snap/src/utils/config.ts @@ -63,6 +63,7 @@ const initialPermissions: DappPermissions = { queryCredentials: false, saveCredential: false, createPresentation: false, + decodeSdJwtPresentation: false, deleteCredential: false, togglePopups: false, addTrustedDapp: false, diff --git a/packages/snap/src/utils/sign.ts b/packages/snap/src/utils/sign.ts index 5f4f6b37e..38c2a8b51 100644 --- a/packages/snap/src/utils/sign.ts +++ b/packages/snap/src/utils/sign.ts @@ -22,7 +22,7 @@ export const sign = async (signArgs: SignArgs, signOptions: SignOptions) => { const { privateKey, did, kid, curve } = signOptions; const ctx = new EC(curve); - const ecPrivateKey = ctx.keyFromPrivate(privateKey.slice(2)); + const ecPrivateKey = ctx.keyFromPrivate(privateKey.slice(2), 'hex'); const jwtPayload = { ...signArgs.payload, diff --git a/packages/snap/src/utils/stateMigration.ts b/packages/snap/src/utils/stateMigration.ts index 19e657eb2..709a514e1 100644 --- a/packages/snap/src/utils/stateMigration.ts +++ b/packages/snap/src/utils/stateMigration.ts @@ -2,6 +2,7 @@ import type { MascaLegacyStateV1, MascaLegacyStateV2, MascaLegacyStateV3, + MascaLegacyStateV4, MascaState, } from '@blockchain-lab-um/masca-types'; import { emptyPolygonBaseState, getInitialPermissions } from './config'; @@ -64,3 +65,18 @@ export const migrateToV4 = (state: MascaLegacyStateV3): MascaState => { return newState as MascaState; }; + +export const migrateToV5 = (state: MascaLegacyStateV4): MascaState => { + const newState: any = { v5: state.v4 }; + + const origins = Object.keys(newState.v5.config.dApp.permissions); + + for (const origin of origins) { + // Add `decodeSdJwtPresentation` to permissions + newState.v5.config.dApp.permissions[ + origin + ].methods.decodeSdJwtPresentation = false; + } + + return newState as MascaState; +}; diff --git a/packages/snap/src/veramo/Veramo.service.ts b/packages/snap/src/veramo/Veramo.service.ts index 255c675b8..7105ce12b 100644 --- a/packages/snap/src/veramo/Veramo.service.ts +++ b/packages/snap/src/veramo/Veramo.service.ts @@ -10,12 +10,15 @@ import { type AvailableCredentialStores, CURRENT_STATE_VERSION, type CreatePresentationRequestParams, + type DecodeSdJwtPresentationRequestParams, type Filter, type MinimalUnsignedCredential, type QueryCredentialsOptions, type QueryCredentialsRequestResult, + type SdJwtCredential, type SaveCredentialRequestResult, type VerifyDataRequestParams, + type Disclosure, } from '@blockchain-lab-um/masca-types'; import { type IOIDCClientPlugin, @@ -58,6 +61,7 @@ import { type W3CVerifiableCredential, createAgent, } from '@veramo/core'; +import type { PresentationFrame } from '@sd-jwt/types'; import { CredentialIssuerEIP712 } from '@veramo/credential-eip712'; import { CredentialStatusPlugin } from '@veramo/credential-status'; import { CredentialPlugin } from '@veramo/credential-w3c'; @@ -103,6 +107,9 @@ import { sign } from '../utils/sign'; import { CeramicCredentialStore } from './plugins/ceramicDataStore/ceramicDataStore'; import { SnapCredentialStore } from './plugins/snapDataStore/snapDataStore'; +import { randomBytes } from 'node:crypto'; +import SDJwtService from 'src/SDJwt.service'; + export type Agent = TAgent< IDIDManager & IKeyManager & @@ -267,6 +274,10 @@ class VeramoService { const { credential, proofFormat = 'jwt' } = params; const identifier = await VeramoService.getIdentifier(); + if (proofFormat === ('sd-jwt' as ProofFormat)) { + return VeramoService.instance.createCredentialSdJwt({ credential }); + } + credential.issuer = identifier.did; const vc = await VeramoService.instance.createVerifiableCredential({ @@ -277,6 +288,63 @@ class VeramoService { return vc; } + static async createCredentialSdJwt(params: { + credential: MinimalUnsignedCredential; + disclosureFrame: Record; + }): Promise { + const sdjwt = SDJwtService.get(); + let { credential, disclosureFrame } = params; + const { did, keys } = await VeramoService.getIdentifier(); + + const sdJwtVcPayload = { + '@context': credential['@context'], + id: randomBytes(16).toString('hex'), + vct: Array.isArray(credential.type) + ? credential.type.join(',') + : credential.type || '', + iss: `${did}#${keys[0].kid}`, + iat: Math.floor(Date.now() / 1000), + sub: `${did}#${keys[0].kid}`, // TODO: Fix this. Here is Holder reference + credentialSubject: { + ...credential.credentialSubject, + }, + credentialSchema: { + ...credential.credentialSchema, + }, + }; + + const credentialSubjectKeys: string[] = Object.keys( + credential.credentialSubject + ); + + disclosureFrame = { + credentialSubject: { + _sd: credentialSubjectKeys, + }, + }; + + const sdJwtCredential = await sdjwt.issue( + sdJwtVcPayload as any, + disclosureFrame as any + ); + + const decode = await sdjwt.decode(sdJwtCredential); + + const signedCredentialWithDisclosures = { + ...decode.jwt?.payload, + signature: decode.jwt?.signature, + encoded: sdJwtCredential, + disclosures: decode.disclosures, + }; + + const customCredential = { + credential: signedCredentialWithDisclosures, + proofType: 'sd-jwt', + }; + + return customCredential; + } + /** * Function that creates an unsigned Verifiable Credential * @param params.credential - Minimal unsigned credential @@ -461,16 +529,55 @@ class VeramoService { static async createPresentation( params: CreatePresentationRequestParams ): Promise { - const { vcs, proofFormat = 'jwt', proofOptions } = params; + const { + vcs, + proofFormat = 'jwt', + proofOptions, + presentationFrame = [], + } = params; const domain = proofOptions?.domain; const challenge = proofOptions?.challenge; const identifier = await VeramoService.getIdentifier(); + if (proofFormat === 'sd-jwt') { + const presentations = await Promise.all( + vcs.map(async (vc) => { + if (typeof vc === 'object' && 'id' in vc && 'encodedVc' in vc) { + // filter keys only for this VC + const presentationKeys = presentationFrame + .filter((claimKey) => vc.id === claimKey.split('/')[0]) + .map((claimKey) => claimKey.split('/')[1]) + .filter(Boolean); + + const presentation = await VeramoService.createPresentationSdJwt({ + encodedSdJwtVc: vc.encodedVc, + presentationFrame: presentationKeys as string[], + }); + + return presentation.presentation; + } + }) + ); + + const combinedPresentations = { + presentations: presentations.map((presentation) => { + return { + presentation: presentation, + }; + }), + proof: { + type: 'sd-jwt', + }, + }; + + return combinedPresentations as any; + } + return VeramoService.instance.createVerifiablePresentation({ presentation: { holder: identifier.did, type: ['VerifiablePresentation', 'Custom'], - verifiableCredential: vcs, + verifiableCredential: vcs as W3CVerifiableCredential[], }, proofFormat, domain, @@ -478,6 +585,168 @@ class VeramoService { }); } + /** + * Creates a presentation SD-JWT (Selective Disclosure JSON Web Token) from the given encoded SD-JWT VC (Verifiable Credential). + * + * @param params - An object containing the encoded SD-JWT VC. + * @param params.encodedSdJwtVc - The encoded SD-JWT VC to be used for creating the presentation. + * @param params.presentationFrame - The presentation frame to be used for creating the presentation. + * @returns A promise that resolves to the created SD-JWT VC presentation. + */ + static async createPresentationSdJwt(params: { + encodedSdJwtVc: string; + presentationFrame: string[]; + }): Promise { + const sdjwt = SDJwtService.get(); + + const { did, keys } = await VeramoService.getIdentifier(); + const { encodedSdJwtVc, presentationFrame } = params; + + const presentationKeys = + await VeramoService.createPresentationFrame(presentationFrame); + + // sd_hash is automatically added by the library + const kbPayload = { + iat: Math.floor(Date.now() / 1000), + aud: '', // TODO: Set the audience + nonce: randomBytes(16).toString('hex'), + }; + + const sdJwtPresentation = await sdjwt.present( + encodedSdJwtVc, + presentationKeys, + { + kb: { payload: kbPayload }, + } + ); + + return { presentation: sdJwtPresentation, proof: { type: 'sd-jwt' } }; + } + + /** + * Decodes a given SD-JWT presentation string and returns the corresponding SdJwtCredential. + * + * @param presentation - The SD-JWT presentation string to be decoded. + * @returns A promise that resolves to an SdJwtCredential object. + */ + static async decodeSdJwtPresentation( + params: DecodeSdJwtPresentationRequestParams + ): Promise { + const sdjwt = SDJwtService.get(); + const credentials: SdJwtCredential[] = []; + + const mapDisclosures = (disclosures: any[] = []) => + disclosures.map((disclosure) => ({ + key: disclosure.key ?? '', + salt: disclosure.salt, + value: disclosure.value as string, + digest: disclosure._digest ?? '', + encoded: disclosure.encode(), + })); + + params.presentation.map(async (vp) => { + const res = await sdjwt.decode(vp); + + const payload = res.jwt?.payload; + const signature = res.jwt?.signature ?? ''; + const disclosures = mapDisclosures(res.disclosures); + + const vc = VeramoService.createSdJwtCredentialFromPayload( + payload, + signature, + disclosures + ); + + vc.encoded = vp; + credentials.push(vc); + }); + + return credentials; + } + + /** + * Helper function to convert a jwt VC payload to SdJwtCredential + * @param vc - The VC payload + * @param jwt - The JWT response containing signature + * @returns SdJwtCredential + */ + private static createSdJwtCredentialFromPayload( + vc: any, + signature: string, + disclosures: Disclosure[] + ): SdJwtCredential { + const sdJwtVc: SdJwtCredential = { + iss: typeof vc.iss === 'string' ? vc.iss : '', + iat: typeof vc.iat === 'number' ? vc.iat : '', + sub: typeof vc.sub === 'string' ? vc.sub : '', + vct: typeof vc.vct === 'string' ? vc.vct : '', + '@context': Array.isArray(vc['@context']) + ? (vc['@context'] as string[]) + : [], + credentialSchema: + typeof vc.credentialSchema === 'object' && vc.credentialSchema !== null + ? { + id: (vc.credentialSchema as Record) + ?.id as string, + type: (vc.credentialSchema as Record) + ?.type as string, + } + : { + id: '', + type: '', + }, + credentialSubject: + typeof vc.credentialSubject === 'object' && + vc.credentialSubject !== null + ? (vc.credentialSubject as Record) + : {}, + _sd_alg: typeof vc._sd_alg === 'string' ? vc._sd_alg : '', + id: typeof vc.id === 'string' ? vc.id : '', + signature: typeof signature === 'string' ? signature : '', + disclosures: disclosures.map((disclosure) => ({ + key: disclosure.key, + salt: disclosure.salt, + value: disclosure.value, + digest: disclosure.digest, + encoded: disclosure.encoded, + })), + }; + + return sdJwtVc; + } + + /** + * Converts an array of claim names into a nested PresentationFrame object. + * @param claims - Array of claim names (e.g., ['id', 'data.list.0.r']). + * @returns A nested PresentationFrame object. + */ + static async createPresentationFrame( + claims: string[] + ): Promise>> { + let frame: any = {}; + + claims.forEach((claim) => { + const keys = claim.split('.'); + let current = frame; + + keys.forEach((key, index) => { + if (!current[key]) { + current[key] = index === keys.length - 1 ? true : {}; + } + current = current[key]; + }); + }); + + // Change this if want to add more properties that are not in credentialSubject to the frame + frame = { + credentialSubject: { + ...frame, + }, + }; + + return frame as PresentationFrame>; + } + /** * Function that creates an unsigned Verifiable Presentation * @param params.credentials - Array of Verifiable Credentials to include in the Verifiable Presentation diff --git a/packages/snap/tests/data/legacyStates/index.ts b/packages/snap/tests/data/legacyStates/index.ts index b4351f9df..15af8047d 100644 --- a/packages/snap/tests/data/legacyStates/index.ts +++ b/packages/snap/tests/data/legacyStates/index.ts @@ -1,3 +1,4 @@ export * from './legacyStateV1'; export * from './legacyStateV2'; export * from './legacyStateV3'; +export * from './legacyStateV4'; diff --git a/packages/snap/tests/data/legacyStates/legacyStateV4.ts b/packages/snap/tests/data/legacyStates/legacyStateV4.ts new file mode 100644 index 000000000..9e543e60a --- /dev/null +++ b/packages/snap/tests/data/legacyStates/legacyStateV4.ts @@ -0,0 +1,114 @@ +import type { + MascaLegacyAccountStateV4, + MascaLegacyStateV4, + MascaLegacyAccountConfigV4, + PolygonLegacyBaseStateV4, + PolygonLegacyStateV4, + DappLegacyPermissionsV4, +} from '@blockchain-lab-um/masca-types'; +import cloneDeep from 'lodash.clonedeep'; + +const emptyPolygonBaseState: PolygonLegacyBaseStateV4 = { + credentials: {}, + identities: {}, + profiles: {}, + merkleTreeMeta: [], + merkleTree: {}, +}; + +const emptyPolygonState: PolygonLegacyStateV4 = { + polygonid: { + polygon: { + main: cloneDeep(emptyPolygonBaseState), + amoy: cloneDeep(emptyPolygonBaseState), + }, + }, + iden3: { + polygon: { + main: cloneDeep(emptyPolygonBaseState), + amoy: cloneDeep(emptyPolygonBaseState), + }, + }, +}; + +const emptyAccountState = { + polygon: { + state: emptyPolygonState, + }, + veramo: { + credentials: {}, + }, + general: { + account: { + ssi: { + selectedMethod: 'did:ethr', + storesEnabled: { + snap: true, + ceramic: true, + }, + }, + } as MascaLegacyAccountConfigV4, + }, +} as MascaLegacyAccountStateV4; + +export const getLegacyEmptyAccountStateV4 = () => cloneDeep(emptyAccountState); + +const initialLegacyPermissionsV4: DappLegacyPermissionsV4 = { + trusted: false, + methods: { + queryCredentials: false, + saveCredential: false, + createPresentation: false, + deleteCredential: false, + togglePopups: false, + addTrustedDapp: false, + removeTrustedDapp: false, + getDID: false, + getSelectedMethod: false, + getAvailableMethods: false, + switchDIDMethod: false, + getCredentialStore: false, + setCredentialStore: false, + getAvailableCredentialStores: false, + getAccountSettings: false, + getSnapSettings: false, + getWalletId: false, + resolveDID: false, + createCredential: false, + setCurrentAccount: false, + verifyData: false, + handleCredentialOffer: false, + handleAuthorizationRequest: false, + setCeramicSession: false, + validateStoredCeramicSession: false, + exportStateBackup: false, + importStateBackup: false, + signData: false, + changePermission: false, + addDappSettings: false, + removeDappSettings: false, + }, +}; + +export const getInitialLegacyPermissionsV4 = () => + cloneDeep(initialLegacyPermissionsV4); + +const initialSnapState: MascaLegacyStateV4 = { + v4: { + accountState: {}, + currentAccount: '', + config: { + dApp: { + disablePopups: false, + permissions: { + 'masca.io': getInitialLegacyPermissionsV4(), + }, + }, + snap: { + acceptedTerms: true, + }, + }, + }, +}; + +export const getLegacyStateV4 = () => cloneDeep(initialSnapState); diff --git a/packages/snap/tests/e2e/changePermission.spec.ts b/packages/snap/tests/e2e/changePermission.spec.ts index f0a2bfe21..01f574580 100644 --- a/packages/snap/tests/e2e/changePermission.spec.ts +++ b/packages/snap/tests/e2e/changePermission.spec.ts @@ -24,6 +24,9 @@ describe('changePermission', () => { const spyQuery = vi.spyOn(snapMock.rpcMocks, 'snap_dialog'); const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; await snapMock.rpcMocks.snap_manageState({ operation: 'update', @@ -66,6 +69,9 @@ describe('changePermission', () => { const spyQuery = vi.spyOn(snapMock.rpcMocks, 'snap_dialog'); const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; const initialPermissions2 = getInitialPermissions(); initialPermissions2.methods.queryCredentials = true; @@ -112,9 +118,11 @@ describe('changePermission', () => { it('should change queryPermission to true & not show a popup when querying', async () => { const spyPermission = vi.spyOn(UIService, 'changePermissionDialog'); - const spyQuery = vi.spyOn(snapMock.rpcMocks, 'snap_dialog'); const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; await snapMock.rpcMocks.snap_manageState({ operation: 'update', @@ -180,6 +188,9 @@ describe('changePermission', () => { const spyQuery = vi.spyOn(snapMock.rpcMocks, 'snap_dialog'); const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; const initialPermissions3 = getInitialPermissions(); initialPermissions3.methods.queryCredentials = true; diff --git a/packages/snap/tests/e2e/createVerifiableCredential.spec.ts b/packages/snap/tests/e2e/createVerifiableCredential.spec.ts index ea927fed4..3b54844b1 100644 --- a/packages/snap/tests/e2e/createVerifiableCredential.spec.ts +++ b/packages/snap/tests/e2e/createVerifiableCredential.spec.ts @@ -1,7 +1,8 @@ -import type { - AvailableCredentialStores, - AvailableMethods, - QueryCredentialsRequestResult, +import { + CURRENT_STATE_VERSION, + type AvailableCredentialStores, + type AvailableMethods, + type QueryCredentialsRequestResult, } from '@blockchain-lab-um/masca-types'; import { type Result, isError } from '@blockchain-lab-um/utils'; import type { MetaMaskInpageProvider } from '@metamask/providers'; @@ -41,9 +42,13 @@ describe('createVerifiableCredential', () => { beforeAll(async () => { snapMock = createMockSnap(); + const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; snapMock.rpcMocks.snap_manageState({ operation: 'update', - newState: getDefaultSnapState(account), + newState: defaultState, }); snapMock.rpcMocks.snap_dialog.mockReturnValue(true); @@ -72,7 +77,7 @@ describe('createVerifiableCredential', () => { issuer = switchMethod.data; - await agent.clear({ options: { store: ['snap', 'ceramic'] } }); + await agent.clear({ options: { store: ['snap'] } }); }); describe.each(proofFormats)('Using Proof Format: %s', (proofFormat) => { @@ -151,7 +156,7 @@ describe('createVerifiableCredential', () => { expect.assertions(2); - await agent.clear({ options: { store: ['snap', 'ceramic'] } }); + await agent.clear({ options: { store: ['snap'] } }); } ); }); diff --git a/packages/snap/tests/e2e/createVerifiablePresentation.spec.ts b/packages/snap/tests/e2e/createVerifiablePresentation.spec.ts index 7f4ea9e90..e7d32cf87 100644 --- a/packages/snap/tests/e2e/createVerifiablePresentation.spec.ts +++ b/packages/snap/tests/e2e/createVerifiablePresentation.spec.ts @@ -1,6 +1,7 @@ -import type { - AvailableMethods, - ProofOptions, +import { + CURRENT_STATE_VERSION, + type AvailableMethods, + type ProofOptions, } from '@blockchain-lab-um/masca-types'; import { type Result, isError } from '@blockchain-lab-um/utils'; import type { MetaMaskInpageProvider } from '@metamask/providers'; @@ -54,9 +55,13 @@ describe('createVerifiablePresentation', () => { beforeAll(async () => { snapMock = createMockSnap(); + const defaultState = getDefaultSnapState(account); + defaultState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; snapMock.rpcMocks.snap_manageState({ operation: 'update', - newState: getDefaultSnapState(account), + newState: defaultState, }); snapMock.rpcMocks.snap_dialog.mockReturnValue(true); global.snap = snapMock; @@ -85,7 +90,7 @@ describe('createVerifiablePresentation', () => { issuer = switchMethod.data; - await agent.clear({ options: { store: ['snap', 'ceramic'] } }); + await agent.clear({ options: { store: ['snap'] } }); }); describe.each(proofFormats)('Using Proof Format: %s', (proofFormat) => { @@ -239,9 +244,7 @@ describe('createVerifiablePresentation', () => { throw new Error('Should have failed'); } - expect(vp.error).toBe( - `Error: invalid_argument: $input.vcs[0].issuer, $input.vcs[0].credentialSubject, $input.vcs[0]["@context"], $input.vcs[0].issuanceDate, $input.vcs[0].proof` - ); + expect(vp.error).toBe('Error: invalid_argument: $input.vcs'); expect.assertions(1); }); }); diff --git a/packages/snap/tests/e2e/importStateBackup.spec.ts b/packages/snap/tests/e2e/importStateBackup.spec.ts index 4b376222c..e1e87a8c5 100644 --- a/packages/snap/tests/e2e/importStateBackup.spec.ts +++ b/packages/snap/tests/e2e/importStateBackup.spec.ts @@ -1,6 +1,5 @@ import { CURRENT_STATE_VERSION, - MascaLegacyStateV1, type MascaState, } from '@blockchain-lab-um/masca-types'; import { type Result, isError } from '@blockchain-lab-um/utils'; @@ -28,9 +27,11 @@ import { getLegacyEmptyAccountStateV1, getLegacyEmptyAccountStateV2, getLegacyEmptyAccountStateV3, + getLegacyEmptyAccountStateV4, getLegacyStateV1, getLegacyStateV2, getLegacyStateV3, + getLegacyStateV4, } from '../data/legacyStates'; import { randomUUID } from 'node:crypto'; @@ -315,6 +316,85 @@ describe('importStateBackup', () => { expect.assertions(2); }); + it('Should suceed with v4 empty state', async () => { + const spy = vi.spyOn(StorageService, 'migrateState'); + + const legacyStateV4 = getLegacyStateV4(); + legacyStateV4.v4.accountState[account] = getLegacyEmptyAccountStateV4(); + legacyStateV4.v4.currentAccount = account; + + const encryptedState = await EncryptionService.encrypt( + JSON.stringify(legacyStateV4) + ); + + const importStateBackupResult = (await onRpcRequest({ + origin: 'http://localhost', + request: { + id: 'test-id', + jsonrpc: '2.0', + method: 'importStateBackup', + params: { serializedState: encryptedState }, + }, + })) as Result; + + if (isError(importStateBackupResult)) { + throw new Error(importStateBackupResult.error); + } + + const expectedState = getInitialSnapState(); + expectedState[CURRENT_STATE_VERSION].accountState[account] = + getEmptyAccountState(); + expectedState[CURRENT_STATE_VERSION].currentAccount = account; + + expect(spy).toHaveBeenCalled(); + expect(StorageService.get()).toEqual(expectedState); + expect.assertions(2); + }); + + it('Should suceed with v4 non-empty state (1 credential)', async () => { + const spy = vi.spyOn(StorageService, 'migrateState'); + + const legacyStateV4 = getLegacyStateV4(); + const credentialId = randomUUID(); + legacyStateV4.v4.accountState[account] = getLegacyEmptyAccountStateV4(); + legacyStateV4.v4.currentAccount = account; + legacyStateV4.v4.accountState[account].veramo.credentials = { + [credentialId]: generatedVC, + }; + + const encryptedState = await EncryptionService.encrypt( + JSON.stringify(legacyStateV4) + ); + + const importStateBackupResult = (await onRpcRequest({ + origin: 'http://localhost', + request: { + id: 'test-id', + jsonrpc: '2.0', + method: 'importStateBackup', + params: { serializedState: encryptedState }, + }, + })) as Result; + + if (isError(importStateBackupResult)) { + throw new Error(importStateBackupResult.error); + } + + const expectedState = getInitialSnapState(); + expectedState[CURRENT_STATE_VERSION].accountState[account] = + getEmptyAccountState(); + expectedState[CURRENT_STATE_VERSION].currentAccount = account; + expectedState[CURRENT_STATE_VERSION].accountState[ + account + ].veramo.credentials = { + [credentialId]: generatedVC, + }; + + expect(spy).toHaveBeenCalled(); + expect(StorageService.get()).toEqual(expectedState); + expect.assertions(2); + }); + it('Should suceed with default empty state', async () => { const startState: MascaState = cloneDeep(StorageService.get()); const exportStateBackupResult = (await onRpcRequest({ diff --git a/packages/snap/tests/e2e/verifyData.spec.ts b/packages/snap/tests/e2e/verifyData.spec.ts index d8815c9a0..4c7d94ac2 100644 --- a/packages/snap/tests/e2e/verifyData.spec.ts +++ b/packages/snap/tests/e2e/verifyData.spec.ts @@ -15,6 +15,7 @@ import { EXAMPLE_VC_PAYLOAD } from '../data/credentials'; import { getDefaultSnapState } from '../data/defaultSnapState'; import { createTestVCs } from '../helpers/generateTestVCs'; import { type SnapMock, createMockSnap } from '../helpers/snapMock'; +import { CURRENT_STATE_VERSION } from '@blockchain-lab-um/masca-types'; describe('verifyData', () => { let snapMock: SnapsProvider & SnapMock; @@ -23,9 +24,15 @@ describe('verifyData', () => { beforeAll(async () => { snapMock = createMockSnap(); + const defaultSnapState = getDefaultSnapState(account); + + defaultSnapState[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; + snapMock.rpcMocks.snap_manageState({ operation: 'update', - newState: getDefaultSnapState(account), + newState: defaultSnapState, }); snapMock.rpcMocks.snap_dialog.mockReturnValue(true); global.snap = snapMock; @@ -62,7 +69,7 @@ describe('verifyData', () => { }); beforeEach(async () => { - await agent.clear({ options: { store: ['snap', 'ceramic'] } }); + await agent.clear({ options: { store: ['snap'] } }); }); it('should succeed verifiying a VC', async () => { diff --git a/packages/snap/tests/unit/ceramic.spec.ts b/packages/snap/tests/unit/ceramic.spec.ts index 3f5a1c313..5885980ac 100644 --- a/packages/snap/tests/unit/ceramic.spec.ts +++ b/packages/snap/tests/unit/ceramic.spec.ts @@ -11,7 +11,7 @@ import { getDefaultSnapState } from '../data/defaultSnapState'; import { EXAMPLE_VC } from '../data/verifiable-credentials'; import { type SnapMock, createMockSnap } from '../helpers/snapMock'; -describe('Utils [ceramic]', () => { +describe.skip('Utils [ceramic]', () => { let snapMock: SnapsProvider & SnapMock; beforeAll(async () => { diff --git a/packages/snap/tests/unit/requestParams.spec.ts b/packages/snap/tests/unit/requestParams.spec.ts index 106ebcf47..45abe2ec8 100644 --- a/packages/snap/tests/unit/requestParams.spec.ts +++ b/packages/snap/tests/unit/requestParams.spec.ts @@ -286,6 +286,14 @@ describe('Utils [requestParams]', () => { }) ).not.toThrowError(); }); + it('request with proofFormat', () => { + expect(() => + isValidCreatePresentationRequest({ + vcs: [EXAMPLE_VC], + proofFormat: 'sd-jwt', + }) + ).not.toThrowError(); + }); it('request with proofFormat and empty proofOptions', () => { expect(() => isValidCreatePresentationRequest({ @@ -295,6 +303,15 @@ describe('Utils [requestParams]', () => { }) ).not.toThrowError(); }); + it('request with proofFormat and empty proofOptions', () => { + expect(() => + isValidCreatePresentationRequest({ + vcs: [EXAMPLE_VC2], + proofFormat: 'sd-jwt', + proofOptions: {}, + }) + ).not.toThrowError(); + }); it('request with proofFormat and proofOptions with domain and challenge', () => { expect(() => isValidCreatePresentationRequest({ @@ -304,6 +321,15 @@ describe('Utils [requestParams]', () => { }) ).not.toThrowError(); }); + it('request with proofFormat and proofOptions with domain and challenge', () => { + expect(() => + isValidCreatePresentationRequest({ + vcs: [EXAMPLE_VC], + proofFormat: 'sd-jwt', + proofOptions: { domain: 'test', challenge: 'test' }, + }) + ).not.toThrowError(); + }); // TODO fix test using ids for vcs it('complete request', () => { expect(() => @@ -563,6 +589,39 @@ describe('Utils [requestParams]', () => { ) ).not.toThrowError(); }); + it('unsignedVC & PF', () => { + const state = getDefaultSnapState(account); + state[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; + expect(() => + isValidCreateCredentialRequest( + { + minimalUnsignedCredential: EXAMPLE_VC_PAYLOAD, + proofFormat: 'sd-jwt', + }, + account, + state + ) + ).not.toThrowError(); + }); + it('empty options', () => { + const state = getDefaultSnapState(account); + state[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; + expect(() => + isValidCreateCredentialRequest( + { + minimalUnsignedCredential: EXAMPLE_VC_PAYLOAD, + proofFormat: 'sd-jwt', + options: {}, + }, + account, + state + ) + ).not.toThrowError(); + }); it('empty options', () => { const state = getDefaultSnapState(account); state[CURRENT_STATE_VERSION].accountState[ @@ -580,6 +639,23 @@ describe('Utils [requestParams]', () => { ) ).not.toThrowError(); }); + it('save option', () => { + const state = getDefaultSnapState(account); + state[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; + expect(() => + isValidCreateCredentialRequest( + { + minimalUnsignedCredential: EXAMPLE_VC_PAYLOAD, + proofFormat: 'sd-jwt', + options: { save: true }, + }, + account, + state + ) + ).not.toThrowError(); + }); it('save option', () => { const state = getDefaultSnapState(account); state[CURRENT_STATE_VERSION].accountState[ @@ -597,6 +673,23 @@ describe('Utils [requestParams]', () => { ) ).not.toThrowError(); }); + it('full options', () => { + const state = getDefaultSnapState(account); + state[CURRENT_STATE_VERSION].accountState[ + account + ].general.account.ssi.storesEnabled.ceramic = false; + expect(() => + isValidCreateCredentialRequest( + { + minimalUnsignedCredential: EXAMPLE_VC_PAYLOAD, + proofFormat: 'sd-jwt', + options: { save: true, store: ['snap'] }, + }, + account, + state + ) + ).not.toThrowError(); + }); it('full options', () => { const state = getDefaultSnapState(account); state[CURRENT_STATE_VERSION].accountState[ diff --git a/packages/snap/vite.config.mts b/packages/snap/vite.config.mts index 8ff8b28d7..04433a906 100644 --- a/packages/snap/vite.config.mts +++ b/packages/snap/vite.config.mts @@ -10,7 +10,7 @@ export default defineConfig({ include: process.env.CRON ? ['tests/cron/**/*.spec.ts'] : ['tests/e2e/**/*.spec.ts', 'tests/unit/**/*.spec.ts'], - silent: true, + silent: false, cache: false, environment: 'node', // or 'happy-dom', 'jsdom' server: { diff --git a/packages/types/package.json b/packages/types/package.json index 123dc53cb..681bacf96 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -26,6 +26,7 @@ "dependencies": { "@0xpolygonid/js-sdk": "1.12.0", "@blockchain-lab-um/utils": "1.4.0-beta.1", + "@sd-jwt/core": "^0.7.2", "@veramo/core": "6.0.0", "typia": "^6.11.2" }, diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 158b40312..022c1c9b1 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -14,12 +14,14 @@ import type { import type { CreateCredentialRequestParams, CreatePresentationRequestParams, + DecodeSdJwtPresentationRequestParams, DeleteCredentialsOptions, HandleAuthorizationRequestParams, HandleCredentialOfferRequestParams, ImportStateBackupRequestParams, QueryCredentialsRequestParams, SaveCredentialOptions, + SdJwtCredential, SetCurrentAccountRequestParams, SignDataRequestParams, VerifyDataRequestParams, @@ -41,6 +43,9 @@ export interface MascaApi { createPresentation( params: CreatePresentationRequestParams ): Promise>; + decodeSdJwtPresentation( + params: DecodeSdJwtPresentationRequestParams + ): Promise>; togglePopups(): Promise>; getDID(): Promise>; getSelectedMethod(): Promise>; diff --git a/packages/types/src/constants.ts b/packages/types/src/constants.ts index 859dd8e39..8063bccd7 100644 --- a/packages/types/src/constants.ts +++ b/packages/types/src/constants.ts @@ -10,7 +10,7 @@ export type AvailableCredentialStores = export const isavailableCredentialStores = (x: string) => isIn(availableCredentialStores, x); -export const CURRENT_STATE_VERSION = 'v4'; +export const CURRENT_STATE_VERSION = 'v5'; /** * @description @@ -121,6 +121,7 @@ export const methodIndexMapping: Record = { export const supportedProofFormats = [ 'jwt', + 'sd-jwt', 'lds', 'EthereumEip712Signature2021', ] as const; diff --git a/packages/types/src/legacy/index.ts b/packages/types/src/legacy/index.ts index e2e5310e7..4806a1cde 100644 --- a/packages/types/src/legacy/index.ts +++ b/packages/types/src/legacy/index.ts @@ -1,3 +1,4 @@ export * from './stateV1'; export * from './stateV2'; export * from './stateV3'; +export * from './stateV4'; diff --git a/packages/types/src/legacy/stateV4.ts b/packages/types/src/legacy/stateV4.ts new file mode 100644 index 000000000..09ec74c03 --- /dev/null +++ b/packages/types/src/legacy/stateV4.ts @@ -0,0 +1,124 @@ +import type { IdentityMerkleTreeMetaInformation } from '@0xpolygonid/js-sdk'; + +import type { W3CVerifiableCredential } from '@veramo/core'; + +import type { + AvailableCredentialStores, + AvailableMethods, +} from '../constants.js'; + +export type LegacyMethodV4 = + | 'queryCredentials' + | 'saveCredential' + | 'createPresentation' + | 'deleteCredential' + | 'togglePopups' + | 'addTrustedDapp' + | 'removeTrustedDapp' + | 'getDID' + | 'getSelectedMethod' + | 'getAvailableMethods' + | 'switchDIDMethod' + | 'getCredentialStore' + | 'setCredentialStore' + | 'getAvailableCredentialStores' + | 'getAccountSettings' + | 'getSnapSettings' + | 'getWalletId' + | 'resolveDID' + | 'createCredential' + | 'setCurrentAccount' + | 'verifyData' + | 'handleCredentialOffer' + | 'handleAuthorizationRequest' + | 'setCeramicSession' + | 'validateStoredCeramicSession' + | 'exportStateBackup' + | 'importStateBackup' + | 'signData' + | 'changePermission' + | 'addDappSettings' + | 'removeDappSettings'; + +export type MethodLegacyPermissionsV4 = { + [key in LegacyMethodV4]: boolean; +}; + +export interface DappLegacyPermissionsV4 { + methods: MethodLegacyPermissionsV4; + trusted: boolean; +} + +export interface MascaLegacyConfigV4 { + snap: { + acceptedTerms: boolean; + }; + dApp: { + disablePopups: boolean; + permissions: Record; + }; +} + +export interface MascaLegacyAccountConfigV4 { + ssi: { + selectedMethod: AvailableMethods; + storesEnabled: Record; + }; +} + +export interface MascaLegacyStateV4 { + /** + * Version 4 of Masca state + */ + v4: { + accountState: Record; + currentAccount: string; + config: MascaLegacyConfigV4; + }; +} + +export interface MascaLegacyAccountStateV4 { + polygon: { + state: PolygonLegacyStateV4; + }; + veramo: { + credentials: Record; + }; + general: { + account: MascaLegacyAccountConfigV4; + ceramicSession?: string; + }; +} + +export interface PolygonLegacyBaseStateV4 { + credentials: Record; + identities: Record; + profiles: Record; + merkleTreeMeta: IdentityMerkleTreeMetaInformation[]; + merkleTree: Record; +} + +export enum DidMethodLegacyV4 { + Iden3 = 'iden3', + PolygonId = 'polygonid', +} + +export enum BlockchainLegacyV4 { + Polygon = 'polygon', +} + +export enum NetworkIdLegacyV4 { + Main = 'main', + Amoy = 'amoy', +} + +export type PolygonLegacyStateV4 = Record< + DidMethodLegacyV4.Iden3 | DidMethodLegacyV4.PolygonId, + Record< + BlockchainLegacyV4.Polygon, + Record< + NetworkIdLegacyV4.Main | NetworkIdLegacyV4.Amoy, + PolygonLegacyBaseStateV4 + > + > +>; diff --git a/packages/types/src/methods.ts b/packages/types/src/methods.ts index 575c883b2..39bfd99c4 100644 --- a/packages/types/src/methods.ts +++ b/packages/types/src/methods.ts @@ -4,6 +4,7 @@ import type { ChangePermissionsRequestParams, CreateCredentialRequestParams, CreatePresentationRequestParams, + DecodeSdJwtPresentationRequestParams, DeleteCredentialsRequestParams, HandleAuthorizationRequestParams, HandleCredentialOfferRequestParams, @@ -46,6 +47,11 @@ export interface CreatePresentation { params: CreatePresentationRequestParams; } +export interface DecodeSdJwtPresentation { + method: 'decodeSdJwtPresentation'; + params: DecodeSdJwtPresentationRequestParams; +} + export interface SetCredentialStore { method: 'setCredentialStore'; params: SetCredentialStoreRequestParams; diff --git a/packages/types/src/params.ts b/packages/types/src/params.ts index 6ee3dfe60..e475bc04f 100644 --- a/packages/types/src/params.ts +++ b/packages/types/src/params.ts @@ -11,6 +11,7 @@ import type { SupportedProofFormats, } from './constants.js'; import type { SignJWTParams, SignJWZParams } from './signData.js'; +import type { SdJwtPayload } from '@sd-jwt/core'; /** * Types @@ -34,6 +35,34 @@ export interface DeleteCredentialsOptions { store?: AvailableCredentialStores | AvailableCredentialStores[]; } +export interface Disclosure { + key: string; + salt: string; + value: string; + digest: string; + encoded: string; +} + +export interface SdJwtCredential extends SdJwtPayload { + iss: string; + iat?: number; + sub?: string; + vct?: string; + '@context'?: string[]; + credentialSchema?: { + id: string; + type: string; + }; + credentialSubject?: Record; + _sd_alg?: string; + id?: string; + signature?: string; + encoded?: string; + disclosures?: Disclosure[]; + + proof?: { type: string }; +} + // TODO (martin): This type is also in datamanager export interface Filter { type: 'none' | 'id' | 'JSONPath'; @@ -59,9 +88,14 @@ export interface CreatePresentationRequestParams { /* * @minItems 1 */ - vcs: W3CVerifiableCredential[]; + vcs: W3CVerifiableCredential[] | Array<{ id: string; encodedVc: string }>; proofFormat?: SupportedProofFormats; proofOptions?: ProofOptions; + presentationFrame?: string[]; +} + +export interface DecodeSdJwtPresentationRequestParams { + presentation: string[]; } export type MinimalUnsignedCredential = Pick< diff --git a/packages/types/src/requests.ts b/packages/types/src/requests.ts index a3d6d7fc6..1bb3d70da 100644 --- a/packages/types/src/requests.ts +++ b/packages/types/src/requests.ts @@ -4,6 +4,7 @@ import type { ChangePermission, CreateCredential, CreatePresentation, + DecodeSdJwtPresentation, DeleteCredential, ExportStateBackup, GetAccountSettings, @@ -36,6 +37,7 @@ export type MascaRPCRequest = | QueryCredentials | SaveCredential | CreatePresentation + | DecodeSdJwtPresentation | DeleteCredential | TogglePopups | AddTrustedDapp diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b320f46c..15ede43bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,12 @@ importers: '@blockchain-lab-um/utils': specifier: 1.4.0-beta.1 version: link:../utils + '@sd-jwt/crypto-nodejs': + specifier: ^0.7.2 + version: 0.7.2 + '@sd-jwt/sd-jwt-vc': + specifier: ^0.8.0 + version: 0.8.0 '@veramo/core': specifier: 6.0.0 version: 6.0.0 @@ -486,6 +492,9 @@ importers: '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sd-jwt/crypto-browser': + specifier: ^0.7.2 + version: 0.7.2 '@supabase/supabase-js': specifier: ^2.43.1 version: 2.43.1(bufferutil@4.0.8) @@ -504,6 +513,9 @@ importers: '@types/jsdom': specifier: ^21.1.6 version: 21.1.6 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 '@veramo/core': specifier: 6.0.0 version: 6.0.0 @@ -837,6 +849,18 @@ importers: '@metamask/utils': specifier: 9.3.0 version: 9.3.0 + '@sd-jwt/core': + specifier: ^0.7.2 + version: 0.7.2 + '@sd-jwt/crypto-nodejs': + specifier: ^0.7.2 + version: 0.7.2 + '@sd-jwt/sd-jwt-vc': + specifier: ^0.8.0 + version: 0.8.0 + '@sd-jwt/types': + specifier: ^0.7.2 + version: 0.7.2 '@sphereon/pex': specifier: 3.3.3 version: 3.3.3 @@ -988,6 +1012,9 @@ importers: '@blockchain-lab-um/utils': specifier: 1.4.0-beta.1 version: link:../../libs/utils + '@sd-jwt/core': + specifier: ^0.7.2 + version: 0.7.2 '@veramo/core': specifier: 6.0.0 version: 6.0.0 @@ -6824,22 +6851,77 @@ packages: '@scure/bip39@1.2.2': resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} + '@sd-jwt/core@0.7.2': + resolution: {integrity: sha512-vix1GplUFc1A9H42r/yXkg7cKYthggyqZEwlFdsBbn4xdZNE+AHVF4N7kPa1pPxipwN3UIHd4XnQ5MJV15mhsQ==} + engines: {node: '>=18'} + + '@sd-jwt/core@0.8.0': + resolution: {integrity: sha512-r1bwTwEXrMTy/+IOFe6ZyhfhwrAqDKBDiB3oj6C/7peF1cNvoHWFkSTPOj/4E26RpAmLaiZH/jjnv3UcCBcSCA==} + engines: {node: '>=18'} + + '@sd-jwt/crypto-browser@0.7.2': + resolution: {integrity: sha512-3EsFaVxgzWw/MguUKjMnW66kBv3NjErgdrf0wniyIAfKCi/njlJ+Zxlj9BW2Dmekiqdh2rH1JOPnCM3CZU9XUw==} + + '@sd-jwt/crypto-nodejs@0.7.2': + resolution: {integrity: sha512-7DHy1WBHwvXseiX+U7XA6jX4dX4Ins3Nxd12JhBSm+FJfIwU97FU/H0KlF6lLyi4a4nbY/O6U9wJjYI1PxA9sQ==} + engines: {node: '>=18'} + '@sd-jwt/decode@0.6.1': resolution: {integrity: sha512-QgTIoYd5zyKKLgXB4xEYJTrvumVwtsj5Dog0v0L9UH9ZvHekDaeexS247X7A4iSdzTvmZzUpGskgABOa4D8NmQ==} engines: {node: '>=16'} + '@sd-jwt/decode@0.7.2': + resolution: {integrity: sha512-dan2LSvK63SKwb62031G4r7TE4TaiI0EK1KbPXqS+LCXNkNDUHqhtYp9uOpj+grXceCsMtMa2f8VnUfsjmwHHg==} + engines: {node: '>=18'} + + '@sd-jwt/decode@0.8.0': + resolution: {integrity: sha512-TLgJDh+2R/xPyrEQuNQdJV3QFbixgLzFt0Fab9SiQsUZkC82b1DdXbtKndFvQ8fVrdNucN/hL27I7Dw4mdguLg==} + engines: {node: '>=18'} + + '@sd-jwt/jwt-status-list@0.8.0': + resolution: {integrity: sha512-PifAbutRSxVV/hzqbMlt4WWuGGrQR+Z21SXrU8F+dNXGa1LMCtKhDVmNhbEVRKj/yVvOK0SIf6u8uhAx1iyrhw==} + engines: {node: '>=18'} + '@sd-jwt/present@0.6.1': resolution: {integrity: sha512-QRD3TUDLj4PqQNZ70bBxh8FLLrOE9mY8V9qiZrJSsaDOLFs2p1CtZG+v9ig62fxFYJZMf4bWKwYjz+qqGAtxCg==} engines: {node: '>=16'} + '@sd-jwt/present@0.7.2': + resolution: {integrity: sha512-mQV85u2+mLLy2VZ9Wx2zpaB6yTDnbhCfWkP7eeCrzJQHBKAAHko8GrylEFmLKewFIcajS/r4lT/zHOsCkp5pZw==} + engines: {node: '>=18'} + + '@sd-jwt/present@0.8.0': + resolution: {integrity: sha512-Yzo/7pOpnLn1I/oxqzP/lDDJg7uCKMq41G3zu0MNO92+DNl3YSUQVV4qoMs5qdnPrSiAeqAhJSmUdUJr+EuprA==} + engines: {node: '>=18'} + + '@sd-jwt/sd-jwt-vc@0.8.0': + resolution: {integrity: sha512-Zl665RFhEyCaoJ8U8+0nu7vOgrDBsA6n8q1R6CEQ9at9RoXSfKyh1mXF3dLvBAL+NZU2vqfQmrRde8I9wxVl+g==} + engines: {node: '>=18'} + '@sd-jwt/types@0.6.1': resolution: {integrity: sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg==} engines: {node: '>=16'} + '@sd-jwt/types@0.7.2': + resolution: {integrity: sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg==} + engines: {node: '>=18'} + + '@sd-jwt/types@0.8.0': + resolution: {integrity: sha512-Tn95AuC9YCWw6RcyUG2pF1iIZbASnczgAWRqQkVJTyhkoBcf68y66HHKxO7K2FJp7ZrhHRm9zeRDb8Om/ZDF2A==} + engines: {node: '>=18'} + '@sd-jwt/utils@0.6.1': resolution: {integrity: sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ==} engines: {node: '>=16'} + '@sd-jwt/utils@0.7.2': + resolution: {integrity: sha512-aMPY7uHRMgyI5PlDvEiIc+eBFGC1EM8OCQRiEjJ8HGN0pajWMYj0qwSw7pS90A49/DsYU1a5Zpvb7nyjgGH0Yg==} + engines: {node: '>=18'} + + '@sd-jwt/utils@0.8.0': + resolution: {integrity: sha512-C6aJ6xQ5Y50U4YP0Ql28hSkHNbMZgzHxVQ49gdbWfhF2E/9psyWtCBKbb8I7mJNBVrWDox1Fmu0oeoBZ2OVPeQ==} + engines: {node: '>=18'} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -8201,6 +8283,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -8220,6 +8310,9 @@ packages: ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + algoliasearch-helper@3.19.0: resolution: {integrity: sha512-AaSb5DZDMZmDQyIy6lf4aL0OZGgyIdqvLIIvSuVQOIOqfhrYSY7TvotIFI2x0Q3cP3xUpTd7lI1astUC4aXBJw==} peerDependencies: @@ -10624,6 +10717,9 @@ packages: fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-url-parser@1.1.3: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} @@ -26843,7 +26939,7 @@ snapshots: chalk: 4.1.2 execa: 5.1.1 metro: 0.80.12(bufferutil@4.0.8)(utf-8-validate@6.0.3) - metro-config: 0.80.12(bufferutil@4.0.8) + metro-config: 0.80.12(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-core: 0.80.12 node-fetch: 2.7.0(encoding@0.1.13) querystring: 0.2.1 @@ -27433,24 +27529,92 @@ snapshots: '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 + '@sd-jwt/core@0.7.2': + dependencies: + '@sd-jwt/decode': 0.7.2 + '@sd-jwt/present': 0.7.2 + '@sd-jwt/types': 0.7.2 + '@sd-jwt/utils': 0.7.2 + + '@sd-jwt/core@0.8.0': + dependencies: + '@sd-jwt/decode': 0.8.0 + '@sd-jwt/present': 0.8.0 + '@sd-jwt/types': 0.8.0 + '@sd-jwt/utils': 0.8.0 + + '@sd-jwt/crypto-browser@0.7.2': {} + + '@sd-jwt/crypto-nodejs@0.7.2': {} + '@sd-jwt/decode@0.6.1': dependencies: '@sd-jwt/types': 0.6.1 '@sd-jwt/utils': 0.6.1 + '@sd-jwt/decode@0.7.2': + dependencies: + '@sd-jwt/types': 0.7.2 + '@sd-jwt/utils': 0.7.2 + + '@sd-jwt/decode@0.8.0': + dependencies: + '@sd-jwt/types': 0.8.0 + '@sd-jwt/utils': 0.8.0 + + '@sd-jwt/jwt-status-list@0.8.0': + dependencies: + '@sd-jwt/types': 0.8.0 + base64url: 3.0.1 + pako: 2.1.0 + '@sd-jwt/present@0.6.1': dependencies: '@sd-jwt/decode': 0.6.1 '@sd-jwt/types': 0.6.1 '@sd-jwt/utils': 0.6.1 + '@sd-jwt/present@0.7.2': + dependencies: + '@sd-jwt/decode': 0.7.2 + '@sd-jwt/types': 0.7.2 + '@sd-jwt/utils': 0.7.2 + + '@sd-jwt/present@0.8.0': + dependencies: + '@sd-jwt/decode': 0.8.0 + '@sd-jwt/types': 0.8.0 + '@sd-jwt/utils': 0.8.0 + + '@sd-jwt/sd-jwt-vc@0.8.0': + dependencies: + '@sd-jwt/core': 0.8.0 + '@sd-jwt/jwt-status-list': 0.8.0 + '@sd-jwt/utils': 0.8.0 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + '@sd-jwt/types@0.6.1': {} + '@sd-jwt/types@0.7.2': {} + + '@sd-jwt/types@0.8.0': {} + '@sd-jwt/utils@0.6.1': dependencies: '@sd-jwt/types': 0.6.1 js-base64: 3.7.7 + '@sd-jwt/utils@0.7.2': + dependencies: + '@sd-jwt/types': 0.7.2 + js-base64: 3.7.7 + + '@sd-jwt/utils@0.8.0': + dependencies: + '@sd-jwt/types': 0.8.0 + js-base64: 3.7.7 + '@sec-ant/readable-stream@0.4.1': {} '@segment/loosely-validate-event@2.0.0': @@ -29684,6 +29848,10 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -29714,6 +29882,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch-helper@3.19.0(algoliasearch@4.23.3): dependencies: '@algolia/events': 4.0.1 @@ -32804,6 +32979,8 @@ snapshots: fast-text-encoding@1.0.6: {} + fast-uri@3.0.6: {} + fast-url-parser@1.1.3: dependencies: punycode: 1.4.1 @@ -35679,6 +35856,21 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.80.12(bufferutil@4.0.8)(utf-8-validate@6.0.3): + dependencies: + connect: 3.7.0 + cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.80.12(bufferutil@4.0.8)(utf-8-validate@6.0.3) + metro-cache: 0.80.12 + metro-core: 0.80.12 + metro-runtime: 0.80.12 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro-core@0.80.12: dependencies: flow-enums-runtime: 0.0.6 @@ -35870,7 +36062,7 @@ snapshots: metro-babel-transformer: 0.80.12 metro-cache: 0.80.12 metro-cache-key: 0.80.12 - metro-config: 0.80.12(bufferutil@4.0.8) + metro-config: 0.80.12(bufferutil@4.0.8)(utf-8-validate@6.0.3) metro-core: 0.80.12 metro-file-map: 0.80.12 metro-resolver: 0.80.12