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

import isCertRevoked from './isCertRevoked';

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)`
 * @param rootCertificates Possible root certificates to complete the path
 */
export default async function validateCertificatePath(
  certificates: string[],
  rootCertificates: string[] = [],
): Promise<boolean> {
  if (rootCertificates.length === 0) {
    // We have no root certs with which to create a full path, so skip path validation
    // TODO: Is this going to be acceptable default behavior??
    return true;
  }

  let invalidSubjectAndIssuerError = false;
  for (const rootCert of rootCertificates) {
    try {
      const certsWithRoot = certificates.concat([rootCert]);
      await _validatePath(certsWithRoot);
      // If we successfully validated a path then there's no need to continue
      invalidSubjectAndIssuerError = false;
      break;
    } catch (err) {
      if (err instanceof InvalidSubjectAndIssuer) {
        invalidSubjectAndIssuerError = true;
      } else {
        throw err;
      }
    }
  }

  // We tried multiple root certs and none of them worked
  if (invalidSubjectAndIssuerError) {
    throw new InvalidSubjectAndIssuer();
  }

  return true;
}

async function _validatePath(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 for certificate revocation
    const subjectCertRevoked = await isCertRevoked(subjectCert);

    if (subjectCertRevoked) {
      throw new Error(`Found revoked certificate in certificate path`);
    }

    // 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 InvalidSubjectAndIssuer();
    }

    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;
}

// Custom errors to help pass on certain errors
class InvalidSubjectAndIssuer extends Error {
  constructor() {
    const message = 'Subject issuer did not match issuer subject';
    super(message);
    this.name = 'InvalidSubjectAndIssuer';
  }
}