Skip to content

Commit

Permalink
feat: integrate sd-jwt (#657)
Browse files Browse the repository at this point in the history
Co-authored-by: martines3000 <[email protected]>
  • Loading branch information
SinanovicEdis and martines3000 committed Jan 29, 2025
1 parent 71a4dd6 commit 8480f01
Show file tree
Hide file tree
Showing 51 changed files with 2,170 additions and 145 deletions.
8 changes: 8 additions & 0 deletions .changeset/seven-tables-serve.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions libs/extended-verification/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions libs/extended-verification/src/Verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export class VerificationService {
VerificationService.veramoAgent = await createVeramoAgent();
}

// TODO: Implement this (edis)
static async verifySdJwtPresentation(args: any): Promise<any> {
throw new Error('Not implemented');
}

static async verify(
data: W3CVerifiablePresentation | W3CVerifiableCredential,
options?: { ebsiChecks?: boolean }
Expand Down
4 changes: 3 additions & 1 deletion libs/extended-verification/src/createVeramoAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export interface CreateVeramoAgentProps {
providers?: Record<string, Provider>;
}

export const createVeramoAgent = async (props?: CreateVeramoAgentProps) => {
export const createVeramoAgent = async (
props?: CreateVeramoAgentProps
): Promise<TAgent<IResolver & ICredentialVerifier>> => {
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
Expand Down
29 changes: 27 additions & 2 deletions packages/connector/src/snap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
AvailableMethods,
CreateCredentialRequestParams,
CreatePresentationRequestParams,
DecodeSdJwtPresentationRequestParams,
DeleteCredentialsOptions,
HandleAuthorizationRequestParams,
HandleCredentialOfferRequestParams,
Expand All @@ -15,6 +16,7 @@ import type {
QueryCredentialsRequestResult,
SaveCredentialOptions,
SaveCredentialRequestResult,
SdJwtCredential,
SetCurrentAccountRequestParams,
SignDataRequestParams,
VerifyDataRequestParams,
Expand Down Expand Up @@ -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<SdJwtCredential> - decoded SD-JWT presentation
*/
async function decodeSdJwtPresentation(
this: Masca,
params: DecodeSdJwtPresentationRequestParams
): Promise<Result<SdJwtCredential[]>> {
return sendSnapMethod<any>(
this,
{
method: 'decodeSdJwtPresentation',
params: {
...params,
},
},
this.snapId
);
}

/**
* Save a VC in Masca under the currently selected MetaMask account
* @param vc - VC to be saved
Expand Down Expand Up @@ -404,11 +427,12 @@ async function createCredential(

const vcResult = result as Result<VerifiableCredential>;

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;
}

Expand Down Expand Up @@ -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)),
Expand Down
2 changes: 2 additions & 0 deletions packages/dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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;
}
Expand All @@ -63,18 +77,32 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => {
case Templates.EduCTX:
return (
<EduCTX
credential={credential}
credential={credential as VerifiableCredential}
title={{
subject: t('subject'),
issuer: t('issuer'),
dates: t('dates'),
}}
/>
);
case Templates.SdJwt:
return (
<SdJwt
credential={credential as SdJwtCredential}
title={{
subject: t('subject'),
issuer: t('issuer'),
dates: t('dates'),
disclosures: t('disclosures'),
}}
viewJsonText={t('view-json')}
selectJsonData={selectJsonData}
/>
);
default:
return (
<Normal
credential={credential}
credential={credential as VerifiableCredential}
title={{
subject: t('subject'),
issuer: t('issuer'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client';

import { VerificationInfoModal } from '@/components/VerificationInfoModal';
import { formatDid } from '@/utils/format';
import { copyToClipboard } from '@/utils/string';
import {
DocumentDuplicateIcon,
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/24/solid';
import { Pagination, Tooltip } from '@nextui-org/react';
import { CheckCircleIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import CredentialPanel from './CredentialPanel';
import type { SdJwtCredential } from '@blockchain-lab-um/masca-connector';
import { usePathname, useRouter } from 'next/navigation';

export const SDJwtView = ({
credential,
page,
total,
}: {
credential: SdJwtCredential;
page: string;
total: number;
}) => {
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 (
<>
<div className="dark:bg-navy-blue-800 relative h-full w-full rounded-3xl bg-white shadow-lg">
<InformationCircleIcon
onClick={() => setVerificationInfoModalOpen(true)}
className="absolute right-3 top-3 h-6 w-6 cursor-pointer"
/>
<div className="dark:from-navy-blue-700 dark:to-navy-blue-700 flex max-w-full flex-col-reverse items-center space-x-4 rounded-t-2xl bg-gradient-to-br from-pink-100 to-orange-100 px-10 pt-6 sm:flex-row">
<div className="flex w-full">
<div className="flex flex-col space-y-4">
<div className="flex flex-col">
<h2 className="dark:text-navy-blue-200 font-bold text-gray-800">
{t('holder')}
</h2>
<h1 className="font-ubuntu dark:text-orange-accent-dark text-left text-lg font-medium text-pink-500 sm:text-xl md:text-2xl lg:truncate">
<div className="mt-2 flex items-center">
<Tooltip
content={holder}
className="border-navy-blue-300 bg-navy-blue-100 text-navy-blue-700"
>
<a
href={`https://dev.uniresolver.io/#${holder}`}
target="_blank"
rel="noreferrer"
className="font-ubuntu dark:text-orange-accent-dark text-left text-lg font-medium text-pink-500 underline sm:text-xl md:text-2xl lg:truncate"
>
{formatDid(holder)}
</a>
</Tooltip>
ar
<button
type="button"
onClick={() => {
copyToClipboard(holder);
}}
>
<DocumentDuplicateIcon className="animated-transition dark:text-orange-accent-dark ml-1 h-5 w-5 text-pink-500 hover:opacity-80" />
</button>
</div>
</h1>
</div>
{issuanceDate && (
<div className="flex flex-col">
<h2 className="dark:text-navy-blue-200 font-bold text-gray-800">
{t('presented')}
</h2>
{new Date(issuanceDate).toDateString()}
</div>
)}
<div className="flex flex-col">
<h2 className="dark:text-navy-blue-200 font-bold text-gray-800">
{t('credentials')}
</h2>
</div>
</div>
</div>
<div className="flex w-full flex-1 justify-end space-x-1">
<Tooltip
content={
isValid ? t('presentation-valid') : t('presentation-invalid')
}
className="border-navy-blue-300 bg-navy-blue-100 text-navy-blue-700"
>
{isValid ? (
<CheckCircleIcon className="dark:text-orange-accent-dark h-12 w-12 text-pink-500" />
) : (
<ExclamationCircleIcon className="dark:text-orange-accent-dark h-12 w-12 text-pink-500" />
)}
</Tooltip>
<div className="flex flex-col items-end">
<h1 className="font-ubuntu dark:text-orange-accent-dark text-left text-lg font-medium text-pink-500 sm:text-xl md:text-2xl lg:truncate">
{t('credential-status')}
</h1>
<h2 className="dark:text-navy-blue-200 font-bold text-gray-800">
{isValid ? t('valid') : t('invalid')}
</h2>
</div>
</div>
</div>
<div className="px-4 pb-6">
<div className="ml-[14px] flex justify-start px-2">
<Pagination
loop
color="success"
total={total}
initialPage={Number.parseInt(page, 10)}
onChange={(val) => {
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',
}}
/>
</div>
<CredentialPanel credential={credential} />
</div>
</div>
<VerificationInfoModal
isOpen={verificationInfoModalOpen}
setOpen={setVerificationInfoModalOpen}
/>
</>
);
};
Loading

0 comments on commit 8480f01

Please sign in to comment.