-
Notifications
You must be signed in to change notification settings - Fork 206
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
feat: support w3c revocation #2024
Changes from 2 commits
787cce6
cc1975a
67bc551
b946e5b
a3e0165
7975139
95d558c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,9 @@ import type { | |
W3cJsonLdVerifyPresentationOptions, | ||
} from '../W3cCredentialServiceOptions' | ||
import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' | ||
import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' | ||
import type { BitStringStatusListCredential, W3cJsonCredential } from '../models/credential/W3cJsonCredential' | ||
|
||
import * as pako from 'pako' | ||
|
||
import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' | ||
import { CredoError } from '../../../error' | ||
|
@@ -109,10 +111,47 @@ export class W3cJsonLdCredentialService { | |
credential: JsonTransformer.toJSON(options.credential), | ||
suite: suites, | ||
documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), | ||
checkStatus: ({ credential }: { credential: W3cJsonCredential }) => { | ||
checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { | ||
// Only throw error if credentialStatus is present | ||
if (verifyCredentialStatus && 'credentialStatus' in credential) { | ||
throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') | ||
if (Array.isArray(credential.credentialStatus)) { | ||
throw new CredoError( | ||
'Verifying credential status as an array for JSON-LD credentials is currently not supported' | ||
) | ||
} | ||
|
||
// Ensure credentialStatus contains the necessary properties | ||
if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { | ||
throw new CredoError('Invalid credential status format') | ||
} | ||
|
||
const credentialStatusURL = credential.credentialStatus.statusListCredential | ||
const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( | ||
credentialStatusURL, | ||
{ | ||
method: 'GET', | ||
} | ||
) | ||
|
||
if (!bitStringStatusListCredential.ok) { | ||
throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) | ||
} | ||
const bitStringCredential = | ||
(await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential | ||
const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList | ||
const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) | ||
|
||
// Decompress using pako | ||
const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) | ||
const statusListIndex = Number(credential.credentialStatus.statusListIndex) | ||
|
||
if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { | ||
throw new CredoError('Index out of bounds') | ||
} | ||
|
||
if (decodedBitString[statusListIndex] === '1') { | ||
throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) | ||
} | ||
} | ||
return { | ||
verified: true, | ||
|
@@ -265,6 +304,52 @@ export class W3cJsonLdCredentialService { | |
challenge: options.challenge, | ||
domain: options.domain, | ||
documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), | ||
checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { | ||
// Only throw error if credentialStatus is present | ||
if ('credentialStatus' in credential) { | ||
if (Array.isArray(credential.credentialStatus)) { | ||
throw new CredoError( | ||
'Verifying credential status as an array for JSON-LD credentials is currently not supported' | ||
) | ||
} | ||
|
||
// Ensure credentialStatus contains the necessary properties | ||
if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { | ||
throw new CredoError('Invalid credential status format') | ||
} | ||
|
||
const credentialStatusURL = credential.credentialStatus.statusListCredential | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should first check the type fiets to make sure this is a bit string status type. There could be others and those are not supported at the moment. |
||
const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( | ||
credentialStatusURL, | ||
{ | ||
method: 'GET', | ||
} | ||
) | ||
|
||
if (!bitStringStatusListCredential.ok) { | ||
throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) | ||
} | ||
const bitStringCredential = | ||
(await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential | ||
const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList | ||
const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) | ||
|
||
// Decompress using pako | ||
const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) | ||
const statusListIndex = Number(credential.credentialStatus.statusListIndex) | ||
|
||
if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should validate the bitstring status list credential as well |
||
throw new CredoError('Index out of bounds') | ||
} | ||
|
||
if (decodedBitString[statusListIndex] === '1') { | ||
throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) | ||
} | ||
} | ||
return { | ||
verified: true, | ||
} | ||
}, | ||
} | ||
|
||
// this is a hack because vcjs throws if purpose is passed as undefined or null | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,59 @@ export interface W3cJsonCredential { | |
expirationDate?: string | ||
credentialSubject: SingleOrArray<JsonObject> | ||
[key: string]: unknown | ||
credentialStatus?: SingleOrArray<CredentialStatus> | ||
} | ||
|
||
type CredentialStatusType = 'BitstringStatusListEntry' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move all the status list logic related to bit string status list to a separate file? |
||
// The purpose can be anything apart from this as well | ||
export enum CredentialStatusPurpose { | ||
'revocation' = 'revocation', | ||
'suspension' = 'suspension', | ||
'message' = 'message', | ||
} | ||
|
||
export interface StatusMessage { | ||
// a string representing the hexadecimal value of the status prefixed with 0x | ||
status: string | ||
// a string used by software developers to assist with debugging which SHOULD NOT be displayed to end users | ||
message?: string | ||
// We can have some key value pairs as well | ||
[key: string]: unknown | ||
} | ||
|
||
export interface CredentialStatus { | ||
id: string | ||
// Since currenlty we are only trying to support 'BitStringStatusListEntry' | ||
type: CredentialStatusType | ||
statusPurpose: CredentialStatusPurpose | ||
// Unique identifier for the specific credential | ||
statusListIndex: string | ||
// Must be url referencing to a VC of type 'BitstringStatusListCredential' | ||
statusListCredential: string | ||
// The statusSize indicates the size of the status entry in bits | ||
statusSize?: number | ||
// Must be preset if statusPurpose is message | ||
/** | ||
* the length of which MUST equal the number of possible status messages indicated by statusSize | ||
* (e.g., statusMessage array MUST have 2 elements if statusSize has 1 bit, | ||
* 4 elements if statusSize has 2 bits, 8 elements if statusSize has 3 bits, etc.). | ||
*/ | ||
statusMessage?: StatusMessage[] | ||
// An implementer MAY include the statusReference property. If present, its value MUST be a URL or an array of URLs [URL] which dereference to material related to the status | ||
statusReference?: SingleOrArray<string> | ||
} | ||
|
||
// Define an interface for `credentialSubject` | ||
export interface CredentialSubject { | ||
encodedList: string | ||
} | ||
|
||
// Define an interface for the `credential` object that uses `CredentialSubject` | ||
export interface Credential { | ||
credentialSubject: CredentialSubject | ||
} | ||
|
||
// Use the `Credential` interface within `BitStringStatusListCredential` | ||
export interface BitStringStatusListCredential { | ||
credential: Credential | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you extract this into a method and reuse that for both checkStatus methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimoGlastra I have developed a common function for this purpose. However, in the checkStatus method, there are specific conditions related to credential status that are dependent on the payload received from the verifyCredential and verifyPresentation functions. Therefore, the checkStatus method invokes the common function, as it contains the identical logic.