diff --git a/src/index.ts b/src/index.ts index 0616177..b29d1f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,23 @@ export type OCSPStatusConfig = { enableNonce?: boolean; }; +/** + * The reason why a certificate was revoked. + * https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1 + */ +export enum OCSPRevocationReason { + unspecified = 0, + keyCompromise = 1, + caCompromise = 2, + affiliationChanged = 3, + superseded = 4, + cessationOfOperation = 5, + certificateHold = 6, + removeFromCRL = 8, + privilegeWithdrawn = 9, + aACompromise = 10, +} + export type OCSPStatusResponse = { /** * Revocation status of the certificate @@ -58,6 +75,10 @@ export type OCSPStatusResponse = { * The time at which the response was produced. */ producedAt?: Date; + /** + * The revocation reason. Only available if the status is 'revoked' and the OCSP response contains a revocation reason. + */ + revocationReason?: OCSPRevocationReason; }; async function downloadIssuerCert( diff --git a/src/ocsp.ts b/src/ocsp.ts index 37ad07f..2a0fdcd 100644 --- a/src/ocsp.ts +++ b/src/ocsp.ts @@ -1,5 +1,5 @@ import { webcrypto } from 'node:crypto'; -import { GeneralizedTime, OctetString, UTCTime } from 'asn1js'; +import { Constructed, Enumerated, GeneralizedTime, OctetString, UTCTime } from 'asn1js'; import * as pkijs from 'pkijs'; import { OCSPStatusConfig, OCSPStatusResponse } from './index'; @@ -176,14 +176,20 @@ export async function parseOCSPResponse( } if (status === 'revoked' && Array.isArray(singleResponse.certStatus?.valueBlock?.value)) { - for (const v of basicResponse.tbsResponseData.responses[0].certStatus.valueBlock.value) { + for (const v of singleResponse.certStatus.valueBlock.value) { if (v instanceof GeneralizedTime) { result.revocationTime = v.toDate(); - break; } if (v instanceof UTCTime) { result.revocationTime = v.toDate(); - break; + } + if (v instanceof Constructed) { + if (Array.isArray(v.valueBlock.value) && v.valueBlock.value.length === 1) { + const vBlock = v.valueBlock.value[0]; + if (vBlock instanceof Enumerated) { + result.revocationReason = vBlock.valueBlock.valueDec; + } + } } } } diff --git a/test/le-revoked.test.ts b/test/le-revoked.test.ts index 1320b17..404bae7 100644 --- a/test/le-revoked.test.ts +++ b/test/le-revoked.test.ts @@ -1,5 +1,5 @@ import { X509Certificate } from 'crypto'; -import { getCertStatus, getCertURLs } from '../src/index'; +import { getCertStatus, getCertURLs, OCSPRevocationReason } from '../src/index'; import { readCertFile } from './test-helper'; let cert: string; @@ -14,6 +14,7 @@ test('Check revoked Lets Encrypt cert', async () => { const result = await getCertStatus(cert); expect(result.status).toBe('revoked'); expect(result.revocationTime?.getTime()).toBe(1702737476000); + expect(result.revocationReason).toBe(OCSPRevocationReason.superseded); }); test('Get OCSP and issuer URLs', async () => { @@ -44,10 +45,12 @@ test('Set ca manually', async () => { }); expect(result.status).toBe('revoked'); expect(result.revocationTime?.getTime()).toBe(1702737476000); + expect(result.revocationReason).toBe(OCSPRevocationReason.superseded); }); test('Pass X509Certificate object', async () => { const result = await getCertStatus(new X509Certificate(cert)); expect(result.status).toBe('revoked'); expect(result.revocationTime?.getTime()).toBe(1702737476000); + expect(result.revocationReason).toBe(OCSPRevocationReason.superseded); });