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

fix: verify status list and fixes #1872

Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {
getIndyNamespaceFromIndyDid,
getQualifiedDidIndyDid,
getUnQualifiedDidIndyDid,

Check warning on line 19 in packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts

View workflow job for this annotation

GitHub Actions / Validate

'getUnQualifiedDidIndyDid' is defined but never used
getUnqualifiedRevocationRegistryDefinitionId,
isIndyDid,
isUnqualifiedCredentialDefinitionId,
Expand Down
10 changes: 6 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
"@digitalcredentials/jsonld-signatures": "^9.4.0",
"@digitalcredentials/vc": "^6.0.1",
"@multiformats/base-x": "^4.0.1",
"@sd-jwt/core": "^0.6.1",
"@sd-jwt/decode": "^0.6.1",
"@sd-jwt/types": "^0.6.1",
"@sd-jwt/utils": "^0.6.1",
"@sd-jwt/core": "^0.7.0",
"@sd-jwt/decode": "^0.7.0",
"@sd-jwt/jwt-status-list": "^0.7.0",
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "^3.3.2",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-types": "^0.23.0",
Expand Down
208 changes: 150 additions & 58 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ import type { Query } from '../../storage/StorageService'
import type { SDJwt } from '@sd-jwt/core'
import type { Signer, Verifier, HasherSync, PresentationFrame, DisclosureFrame } from '@sd-jwt/types'

import { SDJwtInstance } from '@sd-jwt/core'
import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'
import { uint8ArrayToBase64Url } from '@sd-jwt/utils'
import { injectable } from 'tsyringe'

import { Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto'
import { TypedArrayEncoder, Hasher } from '../../utils'
import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto'
import { CredoError } from '../../error'
import { TypedArrayEncoder, Hasher, nowInSeconds } from '../../utils'
import { fetchWithTimeout } from '../../utils/fetch'
import { DidResolverService, parseDid, getKeyFromVerificationMethod } from '../dids'

import { SdJwtVcError } from './SdJwtVcError'
import { SdJwtVcRecord, SdJwtVcRepository } from './repository'

type SdJwtVcConfig = SDJwtVcInstance['userConfig']

export interface SdJwtVc<
Header extends SdJwtVcHeader = SdJwtVcHeader,
Payload extends SdJwtVcPayload = SdJwtVcPayload
Expand All @@ -44,7 +48,9 @@ export interface CnfPayload {

export interface VerificationResult {
isValid: boolean
isSignatureValid: boolean
isValidJwtPayload?: boolean
isSignatureValid?: boolean
isStatusValid?: boolean
isNotBeforeValid?: boolean
isExpiryTimeValid?: boolean
areRequiredClaimsIncluded?: boolean
Expand Down Expand Up @@ -85,16 +91,25 @@ export class SdJwtVcService {
kid: issuer.kid,
} as const

const sdjwt = new SDJwtInstance({
hasher: this.hasher,
const sdjwt = new SDJwtVcInstance({
...this.getBaseSdJwtConfig(agentContext),
signer: this.signer(agentContext, issuer.key),
hashAlg: 'sha-256',
signAlg: issuer.alg,
saltGenerator: agentContext.wallet.generateNonce,
})

if (!payload.vct || typeof payload.vct !== 'string') {
throw new SdJwtVcError("Missing required parameter 'vct'")
}

const compact = await sdjwt.issue(
{ ...payload, cnf: holderBinding?.cnf, iss: issuer.iss, iat: Math.floor(new Date().getTime() / 1000) },
{
...payload,
cnf: holderBinding?.cnf,
iss: issuer.iss,
iat: nowInSeconds(),
vct: payload.vct,
},
disclosureFrame as DisclosureFrame<Payload>,
{ header }
)
Expand Down Expand Up @@ -133,9 +148,8 @@ export class SdJwtVcService {
agentContext: AgentContext,
{ compactSdJwtVc, presentationFrame, verifierMetadata }: SdJwtVcPresentOptions<Payload>
): Promise<string> {
const sdjwt = new SDJwtInstance({
hasher: this.hasher,
})
const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext))

const sdJwtVc = await sdjwt.decode(compactSdJwtVc)

const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc)
Expand Down Expand Up @@ -167,69 +181,124 @@ export class SdJwtVcService {
public async verify<Header extends SdJwtVcHeader = SdJwtVcHeader, Payload extends SdJwtVcPayload = SdJwtVcPayload>(
agentContext: AgentContext,
{ compactSdJwtVc, keyBinding, requiredClaimKeys }: SdJwtVcVerifyOptions
) {
const sdjwt = new SDJwtInstance({
hasher: this.hasher,
})
const sdJwtVc = await sdjwt.decode(compactSdJwtVc)
if (!sdJwtVc.jwt) {
throw new SdJwtVcError('Invalid sd-jwt-vc state.')
}

const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc))
const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc)
const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined

sdjwt.config({
verifier: this.verifier(agentContext, issuer.key),
kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined,
})
): Promise<
| { isValid: true; verification: VerificationResult; sdJwtVc: SdJwtVc<Header, Payload> }
| { isValid: false; verification: VerificationResult; sdJwtVc?: SdJwtVc<Header, Payload>; error: Error }
> {
const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext))

const verificationResult: VerificationResult = {
isValid: false,
isSignatureValid: false,
}

await sdjwt.verify(compactSdJwtVc, requiredClaimKeys, keyBinding !== undefined)
let sdJwtVc: SDJwt

verificationResult.isValid = true
verificationResult.isSignatureValid = true
verificationResult.areRequiredClaimsIncluded = true
try {
sdJwtVc = await sdjwt.decode(compactSdJwtVc)
if (!sdJwtVc.jwt) throw new CredoError('Invalid sd-jwt-vc')
} catch (error) {
return {
isValid: false,
verification: verificationResult,
error,
}
}

const returnSdJwtVc: SdJwtVc<Header, Payload> = {
payload: sdJwtVc.jwt.payload as Payload,
header: sdJwtVc.jwt.header as Header,
compact: compactSdJwtVc,
prettyClaims: await sdJwtVc.getClaims(this.hasher),
} satisfies SdJwtVc<Header, Payload>

// If keyBinding is present, verify the key binding
try {
if (keyBinding) {
if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) {
throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc')
}
const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc))
const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc)
const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined

sdjwt.config({
verifier: this.verifier(agentContext, issuer.key),
kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined,
})

const requiredKeys = requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct']

// Assert `aud` and `nonce` claims
if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) {
throw new SdJwtVcError('The key binding JWT does not contain the expected audience')
try {
await sdjwt.verify(compactSdJwtVc, requiredKeys, keyBinding !== undefined)

verificationResult.isSignatureValid = true
verificationResult.areRequiredClaimsIncluded = true
verificationResult.isStatusValid = true
} catch (error) {
return {
verification: verificationResult,
error,
isValid: false,
sdJwtVc: returnSdJwtVc,
}
}

if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) {
throw new SdJwtVcError('The key binding JWT does not contain the expected nonce')
try {
JwtPayload.fromJson(returnSdJwtVc.payload).validate()
verificationResult.isValidJwtPayload = true
} catch (error) {
verificationResult.isValidJwtPayload = false

return {
isValid: false,
error,
verification: verificationResult,
sdJwtVc: returnSdJwtVc,
}
}

// If keyBinding is present, verify the key binding
try {
if (keyBinding) {
if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) {
throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc')
}

// Assert `aud` and `nonce` claims
if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) {
throw new SdJwtVcError('The key binding JWT does not contain the expected audience')
}

if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) {
throw new SdJwtVcError('The key binding JWT does not contain the expected nonce')
}

verificationResult.isKeyBindingValid = true
verificationResult.containsExpectedKeyBinding = true
verificationResult.containsRequiredVcProperties = true
verificationResult.isKeyBindingValid = true
verificationResult.containsExpectedKeyBinding = true
verificationResult.containsRequiredVcProperties = true
}
} catch (error) {
verificationResult.isKeyBindingValid = false
verificationResult.containsExpectedKeyBinding = false
verificationResult.isValid = false

return {
isValid: false,
error,
verification: verificationResult,
sdJwtVc: returnSdJwtVc,
}
}
} catch (error) {
verificationResult.isKeyBindingValid = false
verificationResult.containsExpectedKeyBinding = false
verificationResult.isValid = false
return {
isValid: false,
error,
verification: verificationResult,
sdJwtVc: returnSdJwtVc,
}
}

verificationResult.isValid = true
return {
isValid: true,
verification: verificationResult,
sdJwtVc: {
payload: sdJwtVc.jwt.payload as Payload,
header: sdJwtVc.jwt.header as Header,
compact: compactSdJwtVc,
prettyClaims: await sdJwtVc.getClaims(this.hasher),
} satisfies SdJwtVc<Header, Payload>,
sdJwtVc: returnSdJwtVc,
}
}

Expand Down Expand Up @@ -272,10 +341,6 @@ export class SdJwtVcService {
}
}

private get hasher(): HasherSync {
return Hasher.hash
}

/**
* @todo validate the JWT header (alg)
*/
Expand Down Expand Up @@ -448,4 +513,31 @@ export class SdJwtVcService {

throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.")
}

private getBaseSdJwtConfig(agentContext: AgentContext): SdJwtVcConfig {
return {
hasher: this.hasher,
statusListFetcher: this.getStatusListFetcher(agentContext),
saltGenerator: agentContext.wallet.generateNonce,
}
}

private get hasher(): HasherSync {
return Hasher.hash
}

private getStatusListFetcher(agentContext: AgentContext) {
return async (uri: string) => {
const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri)
if (!response.ok) {
throw new CredoError(
`Received invalid response with status ${
response.status
} when fetching status list from ${uri}. ${await response.text()}`
)
}

return await response.text()
}
}
}
Loading
Loading