Skip to content

Commit b9bd214

Browse files
authored
feat: skew for jwt-based credential verification (openwallet-foundation#2588)
Signed-off-by: Timo Glastra <[email protected]>
1 parent 657ec73 commit b9bd214

File tree

18 files changed

+100
-42
lines changed

18 files changed

+100
-42
lines changed

.changeset/every-phones-cross.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@credo-ts/openid4vc": patch
3+
"@credo-ts/core": patch
4+
---
5+
6+
feat: add a (configurable) 30 seconds skew to JWT-based credentials and other JWT object verification. This is to prevent verification errors based on slight deviations in server time. This does not affect non-JWT credentials yet (mDOC, JSON-LD)

packages/core/src/agent/AgentConfig.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DEFAULT_SKEW_TIME } from '../crypto/jose/jwt/JwtPayload'
12
import type { Logger } from '../logger'
23
import { ConsoleLogger, LogLevel } from '../logger'
34
import type { InitConfig } from '../types'
@@ -22,6 +23,10 @@ export class AgentConfig {
2223
return this.initConfig.autoUpdateStorageOnStartup ?? false
2324
}
2425

26+
public get validitySkewSeconds() {
27+
return this.initConfig.validitySkewSeconds ?? DEFAULT_SKEW_TIME
28+
}
29+
2530
public extend(config: Partial<InitConfig>): AgentConfig {
2631
return new AgentConfig({ ...this.initConfig, logger: this.logger, ...config }, this.agentDependencies)
2732
}

packages/core/src/crypto/jose/jwt/JwtPayload.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CredoError } from '../../../error'
2+
import { dateToSeconds } from '../../../utils'
23

34
/**
45
* The maximum allowed clock skew time in seconds. If an time based validation
@@ -7,7 +8,7 @@ import { CredoError } from '../../../error'
78
*
89
* See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
910
*/
10-
const DEFAULT_SKEW_TIME = 300
11+
export const DEFAULT_SKEW_TIME = 30
1112

1213
export interface JwtPayloadJson {
1314
iss?: string
@@ -123,8 +124,15 @@ export class JwtPayload {
123124
* - if `iat` is present, it must be less than now
124125
* - if `exp` is present, it must be greater than now
125126
*/
126-
public validate(options?: { skewTime?: number; now?: number }) {
127-
const { nowSkewedFuture, nowSkewedPast } = getNowSkewed(options?.now, options?.skewTime)
127+
public validate(options?: {
128+
/**
129+
* @deprecated use `skewSeconds` instead
130+
*/
131+
skewTime?: number
132+
skewSeconds?: number
133+
now?: number
134+
}) {
135+
const { nowSkewedFuture, nowSkewedPast } = getNowSkewed(options?.now, options?.skewSeconds ?? options?.skewTime)
128136

129137
// Validate nbf
130138
if (typeof this.nbf !== 'number' && typeof this.nbf !== 'undefined') {
@@ -220,12 +228,12 @@ export class JwtPayload {
220228
}
221229
}
222230

223-
function getNowSkewed(now?: number, skewTime?: number) {
224-
const _now = typeof now === 'number' ? now : Math.floor(Date.now() / 1000)
225-
const _skewTime = typeof skewTime !== 'undefined' && skewTime >= 0 ? skewTime : DEFAULT_SKEW_TIME
231+
function getNowSkewed(now?: number, skewSeconds?: number) {
232+
const _now = now ?? dateToSeconds(new Date())
233+
const _skewSeconds = skewSeconds ?? DEFAULT_SKEW_TIME
226234

227235
return {
228-
nowSkewedPast: _now - _skewTime,
229-
nowSkewedFuture: _now + _skewTime,
236+
nowSkewedPast: _now - _skewSeconds,
237+
nowSkewedFuture: _now + _skewSeconds,
230238
}
231239
}

packages/core/src/crypto/jose/jwt/__tests__/JwtPayload.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ describe('JwtPayload', () => {
3636
const jwtPayload = JwtPayload.fromJson({})
3737

3838
jwtPayload.exp = 123
39-
expect(() => jwtPayload.validate({ now: 200, skewTime: 1 })).toThrow('JWT expired at 123')
40-
expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow()
39+
expect(() => jwtPayload.validate({ now: 200, skewSeconds: 1 })).toThrow('JWT expired at 123')
40+
expect(() => jwtPayload.validate({ now: 100, skewSeconds: 1 })).not.toThrow()
4141

4242
jwtPayload.nbf = 80
43-
expect(() => jwtPayload.validate({ now: 75, skewTime: 1 })).toThrow('JWT not valid before 80')
44-
expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow()
43+
expect(() => jwtPayload.validate({ now: 75, skewSeconds: 1 })).toThrow('JWT not valid before 80')
44+
expect(() => jwtPayload.validate({ now: 100, skewSeconds: 1 })).not.toThrow()
4545

4646
jwtPayload.iat = 90
47-
expect(() => jwtPayload.validate({ now: 85, skewTime: 1 })).toThrow('JWT issued in the future at 90')
48-
expect(() => jwtPayload.validate({ now: 100, skewTime: 1 })).not.toThrow()
47+
expect(() => jwtPayload.validate({ now: 85, skewSeconds: 1 })).toThrow('JWT issued in the future at 90')
48+
expect(() => jwtPayload.validate({ now: 100, skewSeconds: 1 })).not.toThrow()
4949
})
5050

5151
test('throws error for invalid values', () => {

packages/core/src/modules/mdoc/Mdoc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { X509Certificate, X509ModuleConfig } from '../x509'
1717
import { getMdocContext } from './MdocContext'
1818
import { MdocError } from './MdocError'
1919
import type { MdocNameSpaces, MdocSignOptions, MdocVerifyOptions } from './MdocOptions'
20-
import { isMdocSupportedSignatureAlgorithm, mdocSupporteSignatureAlgorithms } from './mdocSupportedAlgs'
20+
import { isMdocSupportedSignatureAlgorithm, mdocSupportedSignatureAlgorithms } from './mdocSupportedAlgs'
2121

2222
/**
2323
* This class represents a IssuerSigned Mdoc Document,
@@ -163,7 +163,7 @@ export class Mdoc {
163163
issuerKey.jwkTypeHumanDescription
164164
}. Key supports algs ${issuerKey.supportedSignatureAlgorithms.join(
165165
', '
166-
)}. mdoc supports algs ${mdocSupporteSignatureAlgorithms.join(', ')}`
166+
)}. mdoc supports algs ${mdocSupportedSignatureAlgorithms.join(', ')}`
167167
)
168168
}
169169

packages/core/src/modules/mdoc/MdocDeviceResponse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type {
2929
MdocDeviceResponseVerifyOptions,
3030
MdocSessionTranscriptOptions,
3131
} from './MdocOptions'
32-
import { isMdocSupportedSignatureAlgorithm, mdocSupporteSignatureAlgorithms } from './mdocSupportedAlgs'
32+
import { isMdocSupportedSignatureAlgorithm, mdocSupportedSignatureAlgorithms } from './mdocSupportedAlgs'
3333
import { nameSpacesRecordToMap } from './mdocUtil'
3434

3535
export class MdocDeviceResponse {
@@ -441,7 +441,7 @@ export class MdocDeviceResponse {
441441
jwk.jwkTypeHumanDescription
442442
}. Key supports algs ${jwk.supportedSignatureAlgorithms.join(
443443
', '
444-
)}. mdoc supports algs ${mdocSupporteSignatureAlgorithms.join(', ')}`
444+
)}. mdoc supports algs ${mdocSupportedSignatureAlgorithms.join(', ')}`
445445
)
446446
}
447447

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../kms'
22

3-
export type MdocSupportedSignatureAlgorithm = (typeof mdocSupporteSignatureAlgorithms)[number]
4-
export const mdocSupporteSignatureAlgorithms = [
3+
export type MdocSupportedSignatureAlgorithm = (typeof mdocSupportedSignatureAlgorithms)[number]
4+
export const mdocSupportedSignatureAlgorithms = [
55
KnownJwaSignatureAlgorithms.ES256,
66
KnownJwaSignatureAlgorithms.ES384,
77
KnownJwaSignatureAlgorithms.ES512,
@@ -11,5 +11,5 @@ export const mdocSupporteSignatureAlgorithms = [
1111
export function isMdocSupportedSignatureAlgorithm(
1212
alg: KnownJwaSignatureAlgorithm
1313
): alg is MdocSupportedSignatureAlgorithm {
14-
return mdocSupporteSignatureAlgorithms.includes(alg as MdocSupportedSignatureAlgorithm)
14+
return mdocSupportedSignatureAlgorithms.includes(alg as MdocSupportedSignatureAlgorithm)
1515
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ export class SdJwtVcService {
326326
requiredClaimKeys: requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct'],
327327
keyBindingNonce: keyBinding?.nonce,
328328
currentDate: dateToSeconds(now ?? new Date()),
329-
skewSeconds: 0,
329+
skewSeconds: agentContext.config.validitySkewSeconds,
330330
})
331331
} catch (error) {
332332
return {
@@ -347,7 +347,7 @@ export class SdJwtVcService {
347347
try {
348348
JwtPayload.fromJson(returnSdJwtVc.payload).validate({
349349
now: dateToSeconds(now ?? new Date()),
350-
skewTime: 0,
350+
skewSeconds: agentContext.config.validitySkewSeconds,
351351
})
352352
} catch (error) {
353353
return {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,11 +1219,12 @@ describe('SdJwtVcService', () => {
12191219
})
12201220

12211221
test('verify expired sd-jwt-vc and fails', async () => {
1222+
// 31 seconds due to the skew of 30 seconds
12221223
Date.prototype.getTime = vi.fn(function () {
1223-
return 1716111919 * 1000 + 1000
1224+
return 1716111919 * 1000 + 31000
12241225
})
12251226
Date.now = vi.fn(function () {
1226-
return 1716111919 * 1000 + 1000
1227+
return 1716111919 * 1000 + 31000
12271228
})
12281229
const verificationResult = await sdJwtVcService.verify(agent.context, {
12291230
compactSdJwtVc: expiredSdJwtVc,

packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export class W3cJwtCredentialService {
109109
: W3cJwtVerifiableCredential.fromSerializedJwt(options.credential)
110110

111111
// Verify the JWT payload (verifies whether it's not expired, etc...)
112-
credential.jwt.payload.validate()
112+
credential.jwt.payload.validate({
113+
skewSeconds: agentContext.config.validitySkewSeconds,
114+
})
113115

114116
validationResults.validations.dataModel = {
115117
isValid: true,
@@ -279,7 +281,9 @@ export class W3cJwtCredentialService {
279281
: W3cJwtVerifiablePresentation.fromSerializedJwt(options.presentation)
280282

281283
// Verify the JWT payload (verifies whether it's not expired, etc...)
282-
presentation.jwt.payload.validate()
284+
presentation.jwt.payload.validate({
285+
skewSeconds: agentContext.config.validitySkewSeconds,
286+
})
283287

284288
// Make sure challenge matches nonce
285289
if (options.challenge !== presentation.jwt.payload.additionalClaims.nonce) {

0 commit comments

Comments
 (0)