summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/helpers/isCertRevoked.ts
blob: a4f8a9d54259c9d2c943f06e9e8bb5d19cc7806b (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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import {
  AsnParser,
  AuthorityKeyIdentifier,
  Certificate,
  CertificateList,
  CRLDistributionPoints,
  id_ce_authorityKeyIdentifier,
  id_ce_cRLDistributionPoints,
  id_ce_subjectKeyIdentifier,
  SubjectKeyIdentifier,
} from '../deps.ts';
import { isoUint8Array } from './iso/index.ts';
import { fetch } from './fetch.ts';

/**
 * 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: Certificate): Promise<boolean> {
  const { extensions } = cert.tbsCertificate;

  if (!extensions) {
    return false;
  }

  let extAuthorityKeyID: AuthorityKeyIdentifier | undefined;
  let extSubjectKeyID: SubjectKeyIdentifier | undefined;
  let extCRLDistributionPoints: CRLDistributionPoints | undefined;

  extensions.forEach((ext) => {
    if (ext.extnID === id_ce_authorityKeyIdentifier) {
      extAuthorityKeyID = AsnParser.parse(
        ext.extnValue,
        AuthorityKeyIdentifier,
      );
    } else if (ext.extnID === id_ce_subjectKeyIdentifier) {
      extSubjectKeyID = AsnParser.parse(ext.extnValue, SubjectKeyIdentifier);
    } else if (ext.extnID === id_ce_cRLDistributionPoints) {
      extCRLDistributionPoints = AsnParser.parse(
        ext.extnValue,
        CRLDistributionPoints,
      );
    }
  });

  // Check to see if we've got cached info for the cert's CA
  let keyIdentifier: string | undefined = undefined;

  if (extAuthorityKeyID && extAuthorityKeyID.keyIdentifier) {
    keyIdentifier = isoUint8Array.toHex(
      new Uint8Array(extAuthorityKeyID.keyIdentifier.buffer),
    );
  } else if (extSubjectKeyID) {
    /**
     * We might be dealing with a self-signed root certificate. Check the
     * Subject key Identifier extension next.
     */
    keyIdentifier = isoUint8Array.toHex(new Uint8Array(extSubjectKeyID.buffer));
  }

  const certSerialHex = isoUint8Array.toHex(
    new Uint8Array(cert.tbsCertificate.serialNumber),
  );

  if (keyIdentifier) {
    const cached = cacheRevokedCerts[keyIdentifier];
    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;
      }
    }
  }

  const crlURL = extCRLDistributionPoints?.[0].distributionPoint?.fullName?.[0]
    .uniformResourceIdentifier;

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

  // Download and read the CRL
  let certListBytes: ArrayBuffer;
  try {
    const respCRL = await fetch(crlURL);
    certListBytes = await respCRL.arrayBuffer();
  } catch (_err) {
    return false;
  }

  let data: CertificateList;
  try {
    data = AsnParser.parse(certListBytes, CertificateList);
  } catch (_err) {
    // Something was malformed with the CRL, so pass
    return false;
  }

  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 = isoUint8Array.toHex(
        new Uint8Array(cert.userCertificate),
      );
      newCached.revokedCerts.push(revokedHex);
    }

    // Cache the results
    if (keyIdentifier) {
      cacheRevokedCerts[keyIdentifier] = newCached;
    }

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

  return false;
}