Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements vc 2.0 and bitstring status list #131

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
package-lock.json

# testing
/coverage
Expand Down Expand Up @@ -37,3 +36,5 @@ docker-compose.yml

# typescript
*.tsbuildinfo

compose.yaml
7 changes: 4 additions & 3 deletions components/CredentialCard/CredentialCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { DateTime, Info } from 'luxon';
import { CompletionDocumentSection } from 'components/CompletionDocumentSection/CompletionDocumentSection';
import { Issuer } from 'components/Issuer/Issuer';
import { IssuerObject, VerifiableCredential } from 'types/credential';
import { IssuerObject, VerifiableCredential } from 'types/credential.d';
import type {CredentialCardProps, CredentialDisplayFields} from './CredentialCard.d';
import styles from './CredentialCard.module.css';
import { InfoBlock } from 'components/InfoBlock/InfoBlock';
import { VerifyIndicator } from 'components/VerifyIndicator/VerifyIndicator';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { getExpirationDate, getIssuanceDate } from 'lib/credentialValidityPeriod';

export const CredentialCard = ({ credential, wasMulti = false }: CredentialCardProps) => {
// TODO: add back IssuerInfoModal
Expand Down Expand Up @@ -132,8 +133,8 @@ const mapCredDataToDisplayValues = (credential?: VerifiableCredential): Credenti
}
const common = {
issuedTo: credential.credentialSubject.name,
issuanceDate: credential.issuanceDate,
expirationDate: credential.expirationDate
issuanceDate: getIssuanceDate(credential),
expirationDate: getExpirationDate(credential)
}
if (credential.type.includes("OpenBadgeCredential") || credential.type.includes("AchievementCredential")){
return {...common,
Expand Down
102 changes: 83 additions & 19 deletions components/ResultLog/ResultLog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useState } from 'react';
import { CredentialError } from 'types/credential';
import { CredentialError } from 'types/credential.d';
import type { ResultLogProps } from './ResultLog.d';
import styles from './ResultLog.module.css';
import { StatusPurpose, hasStatusPurpose } from 'lib/credentialStatus';

enum LogId {
ValidSignature = 'valid_signature',
Expiration = 'expiration',
IssuerDIDResolves = 'issuer_did_resolves',
RevocationStatus = 'revocation_status',
SuspensionStatus = 'suspension_status'
}

export const ResultLog = ({ verificationResult }: ResultLogProps) => {
const [moreInfo, setMoreInfo] = useState(false);
Expand All @@ -27,21 +36,55 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => {
)
}

const logMap = verificationResult.results[0]?.log?.reduce((acc: Record<string, boolean>, log) => {
acc[log.id] = log.valid;
return acc;
}, {}) ?? {};

let hasError = false;
let logMap = null;
let hasKnownError = false;
let shouldShowKnownError = false;
let hasUnknownError = false;
let hasSigningError = false;
let error: CredentialError;
if (verificationResult.results[0].error) {
hasError = true;
error = verificationResult.results[0].error
console.log('Error: ', error);
}
let hasResult = verificationResult.results[0];

if (hasResult) {
let log = []
const result = verificationResult.results[0]
const hasResultLog = !!result.log;
const hasErrorLog = !!result.error?.log
hasKnownError = !!result.error
shouldShowKnownError = !!result.error?.isFatal
if (hasKnownError) {
error = result.error
console.log('Error: ', error);
}

if (hasResultLog) {
log = result.log
} else if (hasErrorLog) {
log = result.error.log
}
logMap = log.reduce((acc: Record<string, boolean>, logEntry: any) => {
acc[logEntry.id] = logEntry.valid;
return acc;
}, {}) ?? {};

hasSigningError = ! logMap[LogId.ValidSignature];

} else {
hasUnknownError = true;
}

const result = () => {
if (hasError) {
if (hasSigningError) {
return (
<div>
<p className={styles.error}>"This credential cannot be verified. Note that the JSON code is sensitive to changes in code text including spaces and characters. Please ensure you have input the correct code." <span className={styles.moreInfoLink} onClick={() => setMoreInfo(!moreInfo)}>More Info</span></p>
{moreInfo && (
<div className={styles.errorContainer}>
<p>Something has changed in the credential so that the electronic signature no longer matches the content. This could be something as simple as inadvertently adding a space.</p>
</div>
)}
</div>
)
} else if (shouldShowKnownError) {
return (
<div>
<p className={styles.error}>There was an error verifing this credential. <span className={styles.moreInfoLink} onClick={() => setMoreInfo(!moreInfo)}>More Info</span></p>
Expand All @@ -52,35 +95,56 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => {
)}
</div>
)
} else if (hasUnknownError) {
<div>
<p className={styles.error}>There was an unknown error verifing this credential. <span className={styles.moreInfoLink} onClick={() => setMoreInfo(!moreInfo)}>More Info</span></p>
{moreInfo && (
<div className={styles.errorContainer}>
<p>"Please try again, or let us know."</p>
</div>
)}
</div>

} else {
const { credential } = verificationResult.results[0];
const hasCredentialStatus = credential.credentialStatus !== undefined;
const hasRevocationStatus = hasStatusPurpose(credential, StatusPurpose.Revocation);
const hasSuspensionStatus = hasStatusPurpose(credential, StatusPurpose.Suspension);
return (
<div className={styles.resultLog}>
<div className={styles.issuer}>
<div className={styles.header}>Issuer</div>
<ResultItem
verified={logMap['issuer_did_resolves'] ?? true}
verified={logMap[LogId.IssuerDIDResolves] ?? true}
positiveMessage="Has been issued by a registered institution:"
negativeMessage="Issuing institution cannot be reached for verification"
negativeMessage="Could not find issuer in registry with given DID."
issuer={true}
/>
</div>
<div className={styles.credential}>
<div className={styles.header}>Credential</div>
<ResultItem
verified={logMap['valid_signature'] ?? true}
verified={logMap[LogId.ValidSignature] ?? true}
positiveMessage="Has a valid digital signature"
negativeMessage="Has an invalid digital signature"
/>
<ResultItem
verified={logMap['expiration'] ?? true}
verified={logMap[LogId.Expiration] ?? true}
positiveMessage="Has not expired"
negativeMessage="Has expired"
/>
{hasCredentialStatus && hasRevocationStatus &&
<ResultItem
verified={logMap['revocation_status'] ?? true}
verified={logMap[LogId.RevocationStatus] ?? true}
positiveMessage="Has not been revoked by issuer"
negativeMessage="Has been revoked by issuer"
/>
/>}
{hasCredentialStatus && hasSuspensionStatus &&
<ResultItem
verified={logMap[LogId.SuspensionStatus] ?? true}
positiveMessage="Has not been suspended by issuer"
negativeMessage="Has been suspended by issuer"
/>}
</div>
</div>
)
Expand Down
39 changes: 39 additions & 0 deletions lib/credentialStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { checkStatus } from '@digitalcredentials/vc-bitstring-status-list';
import { checkStatus as checkStatusLegacy } from '@digitalcredentials/vc-status-list';
import { VerifiableCredential } from 'types/credential.d';

export enum StatusPurpose {
Revocation = 'revocation',
Suspension = 'suspension'
}

export function getCredentialStatusChecker(credential: VerifiableCredential) {
if (!credential.credentialStatus) {
return null;
}
const credentialStatuses = Array.isArray(credential.credentialStatus) ?
credential.credentialStatus :
[credential.credentialStatus];
const [credentialStatus] = credentialStatuses;
switch (credentialStatus.type) {
case 'BitstringStatusListEntry':
return checkStatus;
case 'StatusList2021Entry':
return checkStatusLegacy;
default:
return null;
}
}

export function hasStatusPurpose(
credential: VerifiableCredential,
statusPurpose: StatusPurpose
) {
if (!credential.credentialStatus) {
return false;
}
const credentialStatuses = Array.isArray(credential.credentialStatus) ?
credential.credentialStatus :
[credential.credentialStatus];
return credentialStatuses.some(s => s.statusPurpose === statusPurpose);
}
33 changes: 33 additions & 0 deletions lib/credentialValidityPeriod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
VerifiableCredential,
VerifiableCredentialV1,
VerifiableCredentialV2
} from 'types/credential.d';

function getIssuanceDateV1(credential: VerifiableCredential): string | undefined {
return (credential as VerifiableCredentialV1).issuanceDate;
}

function getIssuanceDateV2(credential: VerifiableCredential): string | undefined {
return (credential as VerifiableCredentialV2).validFrom;
}

export function getIssuanceDate(credential: VerifiableCredential): string | undefined {
const issuanceDateV1 = getIssuanceDateV1(credential);
const issuanceDateV2 = getIssuanceDateV2(credential);
return issuanceDateV2 ?? issuanceDateV1;
}

function getExpirationDateV1(credential: VerifiableCredential): string | undefined {
return (credential as VerifiableCredentialV1).expirationDate;
}

function getExpirationDateV2(credential: VerifiableCredential): string | undefined {
return (credential as VerifiableCredentialV2).validUntil;
}

export function getExpirationDate(credential: VerifiableCredential): string | undefined {
const expirationDateV1 = getExpirationDateV1(credential);
const expirationDateV2 = getExpirationDateV2(credential);
return expirationDateV2 ?? expirationDateV1;
}
Loading