apple / apple/app-store-server-library-node

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

Open
#447 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
382
Forks
79
Avg merge
1d 7h
Merged PRs (30d)
9

Description

### 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:

```ts
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:

```ts
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:

```js
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:

```text
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

- [Affected parser and freshness checks](https://github.com/apple/app-store-server-library-node/blob/bb0c0f874494321ea2d005329c3dc2188e893d41/jws_verification.ts#L401-L425)
- [RFC 6960: OCSP response syntax](https://www.rfc-editor.org/rfc/rfc6960.html#section-4.2.1)
- [RFC 6960: freshness semantics](https://www.rfc-editor.org/rfc/rfc6960.html#section-4.2.2.1)
- [PR #394](https://github.com/apple/app-store-server-library-node/pull/394) corrected the signs of the clock-skew adjustments. It did not change date parsing and is not a duplicate of this report.

Contributor guide

Open the contributing guide

Research direction

Start in jws_verification.ts around SignedDataVerifier.checkOCSPStatus and parseX509Date, then run the build and the supplied jsrsasign reproduction. Verify that supported GeneralizedTime values parse independently of the local timezone, invalid timestamps cannot pass freshness checks, and expired or future-dated OCSP responses are rejected. Add regression coverage for these cases and document the policy for an absent nextUpdate.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.