Skip to content

Commit 1eefb26

Browse files
cre8TimoGlastra
andauthored
Add-skew time (#293)
Signed-off-by: Mirko Mollik <mirko.mollik@eudi.sprind.org> Signed-off-by: Mirko Mollik <mirkomollik@gmail.com> Co-authored-by: Timo Glastra <timo@animo.id>
1 parent 0a2f20b commit 1eefb26

File tree

4 files changed

+74
-24
lines changed

4 files changed

+74
-24
lines changed

packages/core/src/index.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
SDJWTException,
55
uint8ArrayToBase64Url,
66
} from '@sd-jwt/utils';
7-
import { Jwt } from './jwt';
7+
import { Jwt, type VerifierOptions } from './jwt';
88
import { KBJwt } from './kbjwt';
99
import { SDJwt, pack } from './sdjwt';
1010
import {
@@ -86,11 +86,11 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload> {
8686
return jwt;
8787
}
8888

89-
private async VerifyJwt(jwt: Jwt, currentDate: number) {
89+
private async VerifyJwt(jwt: Jwt, options?: VerifierOptions) {
9090
if (!this.userConfig.verifier) {
9191
throw new SDJWTException('Verifier not found');
9292
}
93-
return jwt.verify(this.userConfig.verifier, currentDate);
93+
return jwt.verify(this.userConfig.verifier, options);
9494
}
9595

9696
public async issue<Payload extends ExtendedPayload>(
@@ -277,13 +277,10 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload> {
277277
* This function is for validating the SD JWT
278278
* Checking signature, if provided the iat and exp when provided and return its the claims
279279
* @param encodedSDJwt
280-
* @param currentDate
280+
* @param options
281281
* @returns
282282
*/
283-
public async validate(
284-
encodedSDJwt: string,
285-
currentDate: number = Math.floor(Date.now() / 1000),
286-
) {
283+
public async validate(encodedSDJwt: string, options?: VerifierOptions) {
287284
if (!this.userConfig.hasher) {
288285
throw new SDJWTException('Hasher not found');
289286
}
@@ -294,7 +291,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload> {
294291
throw new SDJWTException('Invalid SD JWT');
295292
}
296293

297-
const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, currentDate);
294+
const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, options);
298295
const claims = await sdjwt.getClaims(hasher);
299296
return { payload: claims, header: verifiedPayloads.header };
300297
}

packages/core/src/jwt.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ export type JwtData<
1212
encoded?: string;
1313
};
1414

15+
/**
16+
* Options for the JWT verifier
17+
*/
18+
export type VerifierOptions = {
19+
/**
20+
* current time in seconds since epoch
21+
*/
22+
currentDate?: number;
23+
24+
/**
25+
* allowed skew for the current time in seconds. Positive value that will lower the iat and nbf checks, and increase the exp check.
26+
*/
27+
skewSeconds?: number;
28+
};
29+
1530
// This class is used to create and verify JWT
1631
// Contains header, payload, and signature
1732
export class Jwt<
@@ -117,21 +132,29 @@ export class Jwt<
117132
* Verify the JWT using the provided verifier function.
118133
* It checks the signature and validates the iat, nbf, and exp claims if they are present.
119134
* @param verifier
120-
* @param currentDate
135+
* @param options - Options for verification, such as current date and skew seconds
121136
* @returns
122137
*/
123-
public async verify(
124-
verifier: Verifier,
125-
currentDate = Math.floor(Date.now() / 1000),
126-
) {
127-
if (this.payload?.iat && (this.payload.iat as number) > currentDate) {
138+
public async verify(verifier: Verifier, options?: VerifierOptions) {
139+
const skew = options?.skewSeconds ? options.skewSeconds : 0;
140+
const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
141+
if (
142+
this.payload?.iat &&
143+
(this.payload.iat as number) - skew > currentDate
144+
) {
128145
throw new SDJWTException('Verify Error: JWT is not yet valid');
129146
}
130147

131-
if (this.payload?.nbf && (this.payload.nbf as number) > currentDate) {
148+
if (
149+
this.payload?.nbf &&
150+
(this.payload.nbf as number) - skew > currentDate
151+
) {
132152
throw new SDJWTException('Verify Error: JWT is not yet valid');
133153
}
134-
if (this.payload?.exp && (this.payload.exp as number) < currentDate) {
154+
if (
155+
this.payload?.exp &&
156+
(this.payload.exp as number) + skew < currentDate
157+
) {
135158
throw new SDJWTException('Verify Error: JWT is expired');
136159
}
137160

packages/core/src/test/jwt.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,39 @@ describe('JWT', () => {
269269
});
270270

271271
try {
272-
await jwt.verify(testVerifier, Math.floor(Date.now() / 1000) + 100);
272+
await jwt.verify(testVerifier, {
273+
currentDate: Math.floor(Date.now() / 1000) + 100,
274+
});
273275
} catch (e: unknown) {
274276
expect(e).toBeInstanceOf(SDJWTException);
275277
expect((e as SDJWTException).message).toBe(
276278
'Verify Error: JWT is expired',
277279
);
278280
}
279281
});
282+
283+
test('verify with skew', async () => {
284+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
285+
const testVerifier: Verifier = async (data: string, sig: string) => {
286+
return Crypto.verify(
287+
null,
288+
Buffer.from(data),
289+
publicKey,
290+
Buffer.from(sig, 'base64url'),
291+
);
292+
};
293+
294+
const jwt = new Jwt({
295+
header: { alg: 'EdDSA' },
296+
payload: { exp: Math.floor(Date.now() / 1000) - 1 },
297+
});
298+
299+
const testSigner: Signer = async (data: string) => {
300+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
301+
return Buffer.from(sig).toString('base64url');
302+
};
303+
304+
await jwt.sign(testSigner);
305+
await jwt.verify(testVerifier, { skewSeconds: 2 });
306+
});
280307
});

packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Jwt, SDJwt, SDJwtInstance } from '@sd-jwt/core';
1+
import { Jwt, SDJwt, SDJwtInstance, type VerifierOptions } from '@sd-jwt/core';
22
import type { DisclosureFrame, Hasher, Verifier } from '@sd-jwt/types';
33
import { SDJWTException } from '@sd-jwt/utils';
44
import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
@@ -110,9 +110,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
110110
*/
111111
async verify(
112112
encodedSDJwt: string,
113+
//TODO: we need to move these values in options, causing a breaking change
113114
requiredClaimKeys?: string[],
114115
requireKeyBindings?: boolean,
115-
currentDate: number = Math.floor(Date.now() / 1000),
116+
options?: VerifierOptions,
116117
) {
117118
// Call the parent class's verify method
118119
const result: VerificationResult = await super
@@ -125,7 +126,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
125126
};
126127
});
127128

128-
await this.verifyStatus(result, currentDate);
129+
await this.verifyStatus(result, options);
129130
if (this.userConfig.loadTypeMetadataFormat) {
130131
await this.verifyVct(result);
131132
}
@@ -302,11 +303,11 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
302303
/**
303304
* Verifies the status of the SD-JWT-VC.
304305
* @param result
305-
* @param currentDate current time in seconds
306+
* @param options
306307
*/
307308
private async verifyStatus(
308309
result: VerificationResult,
309-
currentDate: number,
310+
options?: VerifierOptions,
310311
): Promise<void> {
311312
if (result.payload.status) {
312313
//checks if a status field is present in the payload based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html
@@ -325,8 +326,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
325326
StatusListJWTPayload
326327
>(statusListJWT);
327328
// check if the status list has a valid signature. The presence of the verifier is checked in the parent class.
328-
await slJWT.verify(this.userConfig.verifier as Verifier, currentDate);
329+
await slJWT.verify(this.userConfig.verifier as Verifier, options);
329330

331+
const currentDate =
332+
options?.currentDate ?? Math.floor(Date.now() / 1000);
330333
//check if the status list is expired
331334
if (slJWT.payload?.exp && (slJWT.payload.exp as number) < currentDate) {
332335
throw new SDJWTException('Status list is expired');

0 commit comments

Comments
 (0)