Skip to content

Commit 3f7033a

Browse files
committed
feat: Add TokenStatusListService
Signed-off-by: DaevMithran <[email protected]>
1 parent 879ed2c commit 3f7033a

File tree

4 files changed

+187
-1
lines changed

4 files changed

+187
-1
lines changed

packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ export class SdJwtVcService {
488488
}
489489
}
490490

491-
private async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer, forSigning = false) {
491+
public async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer, forSigning = false) {
492492
if (issuer.method === 'did') {
493493
const parsedDid = parseDid(issuer.didUrl)
494494
if (!parsedDid.fragment) {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { StatusList, getListFromStatusListJWT } from '@sd-jwt/jwt-status-list'
2+
import { isURL } from 'class-validator'
3+
import { CredoError } from 'packages/core/src/error'
4+
import { fetchWithTimeout } from 'packages/core/src/utils/fetch'
5+
import { injectable } from 'tsyringe'
6+
import { AgentContext } from '../../../../agent'
7+
import { JwsService, Jwt, JwtPayload } from '../../../../crypto'
8+
import { parseDid } from '../../../dids'
9+
import { SdJwtVcIssuer } from '../../SdJwtVcOptions'
10+
import { SdJwtVcService } from '../../SdJwtVcService'
11+
import { TokenStatusListError } from './TokenStatusListError'
12+
13+
export interface CreateTokenStatusListOptions {
14+
issuer: SdJwtVcIssuer
15+
name: string
16+
tag: string
17+
size: number
18+
publish: boolean
19+
}
20+
21+
export interface CreateTokenStatusListResult {
22+
jwt: string
23+
uri?: string
24+
}
25+
26+
export interface RevokeIndicesOptions {
27+
issuer: SdJwtVcIssuer
28+
publish: boolean
29+
indices: number[]
30+
}
31+
32+
/**
33+
* @internal
34+
*/
35+
@injectable()
36+
export class TokenStatusListService {
37+
public static type = 'TokenStatusList'
38+
39+
async createStatusList(
40+
agentContext: AgentContext,
41+
options: CreateTokenStatusListOptions
42+
): Promise<CreateTokenStatusListResult> {
43+
const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService)
44+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
45+
46+
const statusList = new StatusList(new Array(options.size).fill(0), 1)
47+
48+
const issuer = await sdJwtService.extractKeyFromIssuer(agentContext, options.issuer, true)
49+
50+
// construct jwt payload
51+
const jwtPayload = new JwtPayload({
52+
iss: issuer.iss,
53+
iat: Math.floor(Date.now() / 1000),
54+
additionalClaims: {
55+
status_list: {
56+
bits: statusList.getBitsPerStatus(),
57+
lst: statusList.compressStatusList(),
58+
},
59+
},
60+
})
61+
62+
// sign JWT
63+
const jwt = await jwsService.createJwsCompact(agentContext, {
64+
payload: jwtPayload,
65+
keyId: issuer.publicJwk.keyId,
66+
protectedHeaderOptions: {
67+
alg: issuer.alg,
68+
typ: 'statuslist+jwt',
69+
},
70+
})
71+
72+
if (options.publish) {
73+
// extended by different registries
74+
const uri = await this.publishStatusList(agentContext, jwt)
75+
return { jwt, uri }
76+
}
77+
78+
return { jwt }
79+
}
80+
81+
async revokeIndex(agentContext: AgentContext, statusListId: string, options: RevokeIndicesOptions): Promise<boolean> {
82+
const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService)
83+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
84+
85+
const currentStatusListJwt = await this.getStatusList(agentContext, statusListId)
86+
const parsedStatusListJwt = Jwt.fromSerializedJwt(currentStatusListJwt)
87+
88+
// extract and validate iss
89+
const iss = parsedStatusListJwt.payload.iss as string
90+
if (options.issuer.method === 'did' && parseDid(options.issuer.didUrl).did !== iss) {
91+
throw new TokenStatusListError('Invalid issuer')
92+
}
93+
if (options.issuer.method === 'x5c' && options.issuer.issuer !== iss) {
94+
throw new TokenStatusListError('Invalid issuer')
95+
}
96+
97+
const header = parsedStatusListJwt.header
98+
99+
const statusList = getListFromStatusListJWT(currentStatusListJwt)
100+
// update indices
101+
for (const revokedIndex of options.indices) {
102+
statusList.setStatus(revokedIndex, 1)
103+
}
104+
105+
const issuer = await sdJwtService.extractKeyFromIssuer(agentContext, options.issuer, true)
106+
107+
// construct jwt payload
108+
const jwtPayload = new JwtPayload({
109+
iss,
110+
iat: Math.floor(Date.now() / 1000),
111+
additionalClaims: {
112+
status_list: {
113+
bits: statusList.getBitsPerStatus(),
114+
lst: statusList.compressStatusList(),
115+
},
116+
},
117+
})
118+
119+
const jwt = await jwsService.createJwsCompact(agentContext, {
120+
payload: jwtPayload,
121+
keyId: issuer.publicJwk.keyId,
122+
protectedHeaderOptions: {
123+
alg: issuer.alg,
124+
typ: header.typ,
125+
},
126+
})
127+
128+
if (options.publish) {
129+
await this.publishStatusList(agentContext, jwt)
130+
}
131+
132+
return true
133+
}
134+
135+
async getStatus(agentContext: AgentContext, statusListId: string, index: number): Promise<number> {
136+
const currentStatusListJwt = await this.getStatusList(agentContext, statusListId)
137+
const statusList = getListFromStatusListJWT(currentStatusListJwt)
138+
return statusList.getStatus(index)
139+
}
140+
141+
async getStatusList(agentContext: AgentContext, statusListId: string): Promise<string> {
142+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
143+
144+
let jwt: string
145+
if (isURL(statusListId)) {
146+
jwt = await this.getStatusListFetcher(agentContext)(statusListId)
147+
} else {
148+
throw new Error('Not supported')
149+
}
150+
151+
const verified = await jwsService.verifyJws(agentContext, { jws: jwt })
152+
153+
// verify jwt
154+
if (!verified.isValid) {
155+
throw new TokenStatusListError('Invalid Jwt in the provided statusListId')
156+
}
157+
return jwt
158+
}
159+
160+
private getStatusListFetcher(agentContext: AgentContext) {
161+
return async (uri: string) => {
162+
const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri, {
163+
headers: {
164+
Accept: 'application/statuslist+jwt',
165+
},
166+
})
167+
168+
if (!response.ok) {
169+
throw new CredoError(
170+
`Received invalid response with status ${
171+
response.status
172+
} when fetching status list from ${uri}. ${await response.text()}`
173+
)
174+
}
175+
176+
return await response.text()
177+
}
178+
}
179+
180+
async publishStatusList(_agentContext: AgentContext, _jwt: string): Promise<string> {
181+
throw new Error('Not implemented')
182+
}
183+
}

packages/core/src/modules/sd-jwt-vc/credential-status/token-status-list/TokenStatusListCredential.ts

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { CredoError } from '../../../../error'
2+
3+
export class TokenStatusListError extends CredoError {}

0 commit comments

Comments
 (0)