summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/helpers/validateCertificatePath.ts
blob: c2b18a40fcf9a5a4be224cb2e721901c85623164 (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
/* eslint-disable @typescript-eslint/ban-ts-comment */
// ASN1HEX exists in the lib, but not typings, I swear
// @ts-ignore 2305
import { KJUR, X509, ASN1HEX, zulutodate } from 'jsrsasign';
import fetch from 'node-fetch';

import { leafCertToASN1Object, asn1ObjectToJSON, JASN1 } from './asn1Utils';

const { crypto } = KJUR;

/**
 * Traverse an array of PEM certificates and ensure they form a proper chain
 * @param certificates Typically the result of `x5c.map(convertASN1toPEM)`
 */
export default async function validateCertificatePath(certificates: string[]): Promise<boolean> {
  if (new Set(certificates).size !== certificates.length) {
    throw new Error('Invalid certificate path: found duplicate certificates');
  }

  // From leaf to root, make sure each cert is issued by the next certificate in the chain
  for (let i = 0; i < certificates.length; i += 1) {
    const subjectPem = certificates[i];

    const subjectCert = new X509();
    subjectCert.readCertPEM(subjectPem);

    let issuerPem = '';
    if (i + 1 >= certificates.length) {
      issuerPem = subjectPem;
    } else {
      issuerPem = certificates[i + 1];
    }

    const issuerCert = new X509();
    issuerCert.readCertPEM(issuerPem);

    // Check that intermediate certificate is within its valid time window
    const notBefore = zulutodate(issuerCert.getNotBefore());
    const notAfter = zulutodate(issuerCert.getNotAfter());

    const now = new Date();
    if (notBefore > now || notAfter < now) {
      throw new Error('Intermediate certificate is not yet valid or expired');
    }

    if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) {
      throw new Error('Invalid certificate path: subject issuer did not match issuer subject');
    }

    const subjectCertStruct = ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
    const alg = subjectCert.getSignatureAlgorithmField();
    const signatureHex = subjectCert.getSignatureValueHex();

    const Signature = new crypto.Signature({ alg });
    Signature.init(issuerPem);
    Signature.updateHex(subjectCertStruct);

    if (!Signature.verify(signatureHex)) {
      throw new Error('Invalid certificate path: invalid signature');
    }
  }

  return true;
}

async function isCertRevoked(cert: X509): Promise<boolean> {
  const certSerialHex = cert.getSerialNumberHex();
  const crlURL = cert.getExtCRLDistributionPointsURI();

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

  const crlCert = new X509();

  // Download the CRL
  try {
    const respCRL = await fetch(crlURL[0]);
    const dataCRL = await respCRL.text();
    console.log(`Reading PEM: ${dataCRL}`);
    crlCert.readCertPEM(dataCRL);
  } catch (err) {
    return false;
  }

  const crlASN1 = leafCertToASN1Object(Buffer.from(cert.hex, 'hex'));
  const crlJSON = asn1ObjectToJSON(crlASN1);

  const root0 = (crlJSON.data as JASN1[])[0];

  if ((root0.data as JASN1[])?.length < 7) {
    // CRL is empty
    return false;
  }

  // Drill down into the ASN structure
  const root05 = (root0.data as JASN1[])[5];
  const revokedCerts = root05.data;

  if (revokedCerts) {
    for (const cert of revokedCerts) {
      const certSerialData = (cert as JASN1).data;
      if (certSerialData) {
        const certSerialSequence = (certSerialData[0] as JASN1).data;
        if (typeof certSerialSequence === 'string') {
          // Grab the value after "\n" in "(115 bit)\n23373519225161898650309958210680307"
          const revokedHex = parseInt(certSerialSequence.split('\n')[1], 10).toString(16);
          console.log(`Checking if cert ${certSerialHex} matches revoked ${revokedHex}`);
          if (certSerialHex === revokedHex) {
            return true;
          }
        }
      }
    }
  }

  return false;
}