summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/helpers/isCertRevoked.ts
blob: cc8c3f1167762cc30a7391caf0c1b42e4ed8918d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { X509 } from 'jsrsasign';
import fetch from 'node-fetch';
import { AsnParser } from '@peculiar/asn1-schema';
import { CertificateList } from '@peculiar/asn1-x509';

import { convertCertBufferToPEM } from './convertCertBufferToPEM';

/**
 * A cache of revoked cert serial numbers by Authority Key ID
 */
type CAAuthorityInfo = {
  // A list of certificates serial numbers in hex format
  revokedCerts: string[];
  // An optional date by which an update should be published
  nextUpdate?: Date;
};
const cacheRevokedCerts: { [certAuthorityKeyID: string]: CAAuthorityInfo } = {};

/**
 * A method to pull a CRL from a certificate and compare its serial number to the list of revoked
 * certificate serial numbers within the CRL.
 *
 * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
 */
export async function isCertRevoked(cert: X509): Promise<boolean> {
  const certSerialHex = cert.getSerialNumberHex();

  // Check to see if we've got cached info for the cert's CA
  let certAuthKeyID: { kid: { hex: string } } | null = null;
  try {
    certAuthKeyID = cert.getExtAuthorityKeyIdentifier() as { kid: { hex: string } } | null;
  } catch (err) {
    return false;
  }

  if (certAuthKeyID) {
    const cached = cacheRevokedCerts[certAuthKeyID.kid.hex];
    if (cached) {
      const now = new Date();
      // If there's a nextUpdate then make sure we're before it
      if (!cached.nextUpdate || cached.nextUpdate > now) {
        return cached.revokedCerts.indexOf(certSerialHex) >= 0;
      }
    }
  }

  let crlURL = undefined;
  try {
    crlURL = cert.getExtCRLDistributionPointsURI();
  } catch (err) {
    // Cert probably didn't include any CDP URIs
    return false;
  }

  // If no URL is provided then we have nothing to check
  if (!crlURL) {
    return false;
  }

  // Download and read the CRL
  const crlCert = new X509();
  try {
    const respCRL = await fetch(crlURL[0]);
    const dataCRL = await respCRL.buffer();
    const dataPEM = convertCertBufferToPEM(dataCRL);
    crlCert.readCertPEM(dataPEM);
  } catch (err) {
    return false;
  }

  const data = AsnParser.parse(Buffer.from(crlCert.hex, 'hex'), CertificateList);

  const newCached: CAAuthorityInfo = {
    revokedCerts: [],
    nextUpdate: undefined,
  };

  // nextUpdate
  if (data.tbsCertList.nextUpdate) {
    newCached.nextUpdate = data.tbsCertList.nextUpdate.getTime();
  }

  // revokedCertificates
  const revokedCerts = data.tbsCertList.revokedCertificates;

  if (revokedCerts) {
    for (const cert of revokedCerts) {
      const revokedHex = Buffer.from(cert.userCertificate).toString('hex');
      newCached.revokedCerts.push(revokedHex);
    }

    // Cache the results
    if (certAuthKeyID) {
      cacheRevokedCerts[certAuthKeyID.kid.hex] = newCached;
    }

    return newCached.revokedCerts.indexOf(certSerialHex) >= 0;
  }

  return false;
}