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

feat: support w3c revocation #2024

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"luxon": "^3.3.0",
"make-error": "^1.3.6",
"object-inspect": "^1.10.3",
"pako": "^2.1.0",
"query-string": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
Expand All @@ -71,6 +72,7 @@
"@types/jsonpath": "^0.2.4",
"@types/luxon": "^3.2.0",
"@types/object-inspect": "^1.8.0",
"@types/pako": "^2.0.3",
"@types/uuid": "^9.0.1",
"@types/varint": "^6.0.0",
"nock": "^13.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -265,6 +304,52 @@ export class W3cJsonLdCredentialService {
challenge: options.challenge,
domain: options.domain,
documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext),
checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => {
Copy link
Contributor

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?

Copy link
Author

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.

// 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,59 @@ export interface W3cJsonCredential {
expirationDate?: string
credentialSubject: SingleOrArray<JsonObject>
[key: string]: unknown
credentialStatus?: SingleOrArray<CredentialStatus>
}

type CredentialStatusType = 'BitstringStatusListEntry'
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
Loading