Skip to content

SignedDataVerifier accepts stale OCSP status information because GeneralizedTime dates parse as NaN #447

Description

@abhinavgautam01

Description

When enableOnlineChecks is enabled, SignedDataVerifier.checkOCSPStatus parses the matching response's thisupdate and nextupdate fields with parseX509Date. The installed jsrsasign OCSP parser preserves the trailing Z in GeneralizedTime values such as 20260905033451Z.

However, parseX509Date only matches 14 digits with no timezone suffix:

private parseX509Date(date: string) {
  return new Date(date.replace(
    /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/,
    '$4:$5:$6 $2/$3/$1'
  ));
}

A value ending in Z does not match the expression and reaches new Date unchanged. Node.js returns Invalid Date, whose timestamp is NaN. Both timestamp comparisons in the following condition then evaluate to false:

if (
  singleResponse.status.status !== 'good' ||
  new Date().getTime() + MAX_SKEW < issueDate.getTime() ||
  nextDate.getTime() < new Date().getTime() - MAX_SKEW
) {
  throw new VerificationException(VerificationStatus.FAILURE)
}

Consequently, an otherwise accepted OCSP response with status good is not rejected when nextUpdate is expired or thisUpdate is in the future. The fallback parsing of values without Z also uses the process's local timezone instead of UTC.

Reproduction

From the repository root, after building the package, run:

const { KJUR } = require('jsrsasign');
const { SignedDataVerifier, Environment } = require('./dist/index.js');

const verifier = new SignedDataVerifier([], true, Environment.SANDBOX, 'com.example');

// Encode and decode a SingleResponse to demonstrate the dependency's actual output.
const encoded = new KJUR.asn1.ocsp.SingleResponse({
  certid: {
    alg: 'sha256',
    issname: '00'.repeat(32),
    isskey: '11'.repeat(32),
    sbjsn: '01',
  },
  status: { status: 'good' },
  thisupdate: '20200101000000Z',
  nextupdate: '20200102000000Z',
});

const response = new KJUR.asn1.ocsp.OCSPParser().getSingleResponse(encoded.tohex());
// Accessing the TypeScript-private method here is possible in emitted JavaScript.
const issueDate = verifier.parseX509Date(response.thisupdate);
const nextDate = verifier.parseX509Date(response.nextupdate);
const now = Date.now();
const MAX_SKEW = 60000;

console.log(response.thisupdate, response.nextupdate);
console.log(issueDate.getTime(), nextDate.getTime());
console.log(
  'Rejected by freshness condition:',
  response.status.status !== 'good' ||
    now + MAX_SKEW < issueDate.getTime() ||
    nextDate.getTime() < now - MAX_SKEW
);

Observed output:

20200101000000Z 20200102000000Z
NaN NaN
Rejected by freshness condition: false

This reproduction isolates ASN.1 decoding, date parsing and the acceptance condition. It does not construct a fully signed Apple OCSP response or demonstrate a production replay attack.

For the timezone issue, parsing 20260905033451 produces 2026-09-05T03:34:51.000Z under TZ=UTC, but 2026-09-04T22:04:51.000Z under TZ=Asia/Kolkata.

Expected behavior

Parse supported GeneralizedTime values in UTC, reject invalid timestamps and enforce the existing freshness bounds with the intended clock-skew allowance.

Impact

This is a failure to reject stale or future-dated OCSP status information when online checks are enabled. Replayed, previously signed good responses could pass the freshness gate if all other verification requirements are satisfied.

OCSP signatures, responder authorization, certificate matching, certificate validity and JWS signatures are checked separately. This finding does not establish a bypass of those checks or acceptance of arbitrary forged transactions.

Suggested change

  • Parse the supported GeneralizedTime format explicitly in UTC.
  • Reject malformed or non-finite timestamps before any comparison. Returning Invalid Date alone would preserve the bypass.
  • Validate calendar components rather than relying solely on Date.UTC, which can normalize out-of-range values.
  • Handle an absent nextUpdate explicitly under a documented policy; the OCSP syntax makes this field optional.
  • Add regression coverage for expired and future-dated responses, malformed dates,and timezone-independent parsing.

References and duplicate check

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions