CVE-2022-31083: Improper Certificate Validation (CWE-295)
value: Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 4.10.11 and 5.2.2, the certificate in the Parse Server Apple Game Center auth adapter not validated. As a result, authentication could potentially be bypassed by making a fake certificate accessible via certain Apple domains and providing the URL to that certificate in an authData object. Versions 4.0.11 and 5.2.2 prevent this by introducing a new `rootCertificateUrl` property to the Parse Server Apple Game Center auth adapter which takes the URL to the root certificate of Apple's Game Center authentication certificate. If no value is set, the `rootCertificateUrl` property defaults to the URL of the current root certificate as of May 27, 2022. Keep in mind that the root certificate can change at any time and that it is the developer's responsibility to keep the root certificate URL up-to-date when using the Parse Server Apple Game Center auth adapter. There are no known workarounds for this issue.
CWE description: The product does not validate, or incorrectly validates, a certificate.
CVSS v3 base score: 7.5
Published: 2022-06-17T19:15Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
Files changed: release.config.js, spec/AuthenticationAdapters.spec.js, spec/support/cert/game_center.pem, src/Adapters/Auth/gcenter.js
Commit date: 2022-06-17 18:29:26 +0200
Commit message: fix: certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc))


Diff: diff --git a/release.config.js b/release.config.js
index af2bd2c9f..c31eb3ecb 100644
--- a/release.config.js
+++ b/release.config.js
@@ -81,25 +81,24 @@ async function config() {
         'npmPublish': true,
       }],
       ['@semantic-release/git', {
         assets: [changelogFile, 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
       }],
+      ['@semantic-release/github', {
+        successComment: getReleaseComment(),
+        labels: ['type:ci'],
+        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
+      }],
       [
         "@saithodev/semantic-release-backmerge",
         {
           "branches": [
             { from: "beta", to: "alpha" },
             { from: "release", to: "beta" },
-            { from: "release", to: "alpha" },
           ]
         }
       ],
-      ['@semantic-release/github', {
-        successComment: getReleaseComment(),
-        labels: ['type:ci'],
-        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
-      }],
     ],
   };
 
   return config;
 }
diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js
index ca2d35363..5c0123f3b 100644
--- a/spec/AuthenticationAdapters.spec.js
+++ b/spec/AuthenticationAdapters.spec.js
@@ -1650,45 +1650,98 @@ describe('apple signin auth adapter', () => {
   });
 });
 
 describe('Apple Game Center Auth adapter', () => {
   const gcenter = require('../lib/Adapters/Auth/gcenter');
-
+  const fs = require('fs');
+  const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
+  it('can load adapter', async () => {
+    const options = {
+      gcenter: {
+        rootCertificateUrl:
+          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
+      },
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
+  });
   it('validateAuthData should validate', async () => {
+    const options = {
+      gcenter: {
+        rootCertificateUrl:
+          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
+      },
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
     // real token is used
     const authData = {
       id: 'G:1965586982',
       publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
       timestamp: 1565257031287,
       signature:
         'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
       salt: 'DzqqrQ==',
       bundleId: 'cloud.xtralife.gamecenterauth',
     };
-
+    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
     await gcenter.validateAuthData(authData);
   });
 
   it('validateAuthData invalid signature id', async () => {
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      {}
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
     const authData = {
       id: 'G:1965586982',
-      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
+      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
       timestamp: 1565257031287,
       signature: '1234',
       salt: 'DzqqrQ==',
-      bundleId: 'cloud.xtralife.gamecenterauth',
+      bundleId: 'com.example.com',
     };
-
-    try {
-      await gcenter.validateAuthData(authData);
-      fail();
-    } catch (e) {
-      expect(e.message).toBe('Apple Game Center - invalid signature');
-    }
+    await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
+      new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
+    );
   });
 
   it('validateAuthData invalid public key http url', async () => {
+    const options = {
+      gcenter: {
+        rootCertificateUrl:
+          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
+      },
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
     const publicKeyUrls = [
       'example.com',
       'http://static.gc.apple.com/public-key/gc-prod-4.cer',
       'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
       'https://example.com/ \\.apple.com/public_key.cer',
@@ -1712,10 +1765,82 @@ describe('Apple Game Center Auth adapter', () => {
           )
         )
       )
     );
   });
+
+  it('should not validate Symantec Cert', async () => {
+    const options = {
+      gcenter: {
+        rootCertificateUrl:
+          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
+      },
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
+    expect(() =>
+      gcenter.verifyPublicKeyIssuer(
+        testCert,
+        'https://static.gc.apple.com/public-key/gc-prod-4.cer'
+      )
+    );
+  });
+
+  it('adapter should load default cert', async () => {
+    const options = {
+      gcenter: {},
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
+    const previous = new Date();
+    await adapter.validateAppId(
+      appIds,
+      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+      providerOptions
+    );
+
+    const duration = new Date().getTime() - previous.getTime();
+    expect(duration).toEqual(0);
+  });
+
+  it('adapter should throw', async () => {
+    const options = {
+      gcenter: {
+        rootCertificateUrl: 'https://example.com',
+      },
+    };
+    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
+      'gcenter',
+      options
+    );
+    await expectAsync(
+      adapter.validateAppId(
+        appIds,
+        { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
+        providerOptions
+      )
+    ).toBeRejectedWith(
+      new Parse.Error(
+        Parse.Error.OBJECT_NOT_FOUND,
+        'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
+      )
+    );
+  });
 });
 
 describe('phant auth adapter', () => {
   const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
 
diff --git a/spec/support/cert/game_center.pem b/spec/support/cert/game_center.pem
new file mode 100644
index 000000000..b5dffcd83
--- /dev/null
+++ b/spec/support/cert/game_center.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/
+MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd
+BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj
+IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa
+Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
+bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w
+DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM
+IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7
+ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA
+17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz
+ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT
+q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC
+ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
+AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k
+LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv
+bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw
+IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE
+SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC
+hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
+I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV
+woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu
+tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz
+vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ
+tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr
+EEbl0YdElmF8Hlamah/yNw==
+-----END CERTIFICATE-----
diff --git a/src/Adapters/Auth/gcenter.js b/src/Adapters/Auth/gcenter.js
index 5cd8e8aff..f70c25418 100644
--- a/src/Adapters/Auth/gcenter.js
+++ b/src/Adapters/Auth/gcenter.js
@@ -12,11 +12,12 @@ const authData = {
 */
 
 const { Parse } = require('parse/node');
 const crypto = require('crypto');
 const https = require('https');
-
+const { pki } = require('node-forge');
+const ca = { cert: null, url: null };
 const cache = {}; // (publicKey -> cert) cache
 
 function verifyPublicKeyUrl(publicKeyUrl) {
   try {
     const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
@@ -50,43 +51,57 @@ async function getAppleCertificate(publicKeyUrl) {
   const headOptions = {
     hostname: url.hostname,
     path: url.pathname,
     method: 'HEAD',
   };
-  const headers = await new Promise((resolve, reject) =>
+  const cert_headers = await new Promise((resolve, reject) =>
     https.get(headOptions, res => resolve(res.headers)).on('error', reject)
   );
+  const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
   if (
-    headers['content-type'] !== 'application/pkix-cert' ||
-    headers['content-length'] == null ||
-    headers['content-length'] > 10000
+    !validContentTypes.includes(cert_headers['content-type']) ||
+    cert_headers['content-length'] == null ||
+    cert_headers['content-length'] > 10000
   ) {
     throw new Parse.Error(
       Parse.Error.OBJECT_NOT_FOUND,
       `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
     );
   }
+  const { certificate, headers } = await getCertificate(publicKeyUrl);
+  if (headers['cache-control']) {
+    const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
+    if (expire) {
+      cache[publicKeyUrl] = certificate;
+      // we'll expire the cache entry later, as per max-age
+      setTimeout(() => {
+        delete cache[publicKeyUrl];
+      }, parseInt(expire[1], 10) * 1000);
+    }
+  }
+  return verifyPublicKeyIssuer(certificate, publicKeyUrl);
+}
+
+function getCertificate(url, buffer) {
   return new Promise((resolve, reject) => {
     https
-      .get(publicKeyUrl, res => {
-        let data = '';
+      .get(url, res => {
+        const data = [];
         res.on('data', chunk => {
-          data += chunk.toString('base64');
+          data.push(chunk);
         });
         res.on('end', () => {
-          const cert = convertX509CertToPEM(data);
-          if (res.headers['cache-control']) {
-            var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
-            if (expire) {
-              cache[publicKeyUrl] = cert;
-              // we'll expire the cache entry later, as per max-age
-              setTimeout(() => {
-                delete cache[publicKeyUrl];
-              }, parseInt(expire[1], 10) * 1000);
-            }
+          if (buffer) {
+            resolve({ certificate: Buffer.concat(data), headers: res.headers });
+            return;
           }
-          resolve(cert);
+          let cert = '';
+          for (const chunk of data) {
+            cert += chunk.toString('base64');
+          }
+          const certificate = convertX509CertToPEM(cert);
+          resolve({ certificate, headers: res.headers });
         });
       })
       .on('error', reject);
   });
 }
@@ -113,10 +128,34 @@ function verifySignature(publicKey, authData) {
   if (!verifier.verify(publicKey, authData.signature, 'base64')) {
     throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - invalid signature');
   }
 }
 
+function verifyPublicKeyIssuer(cert, publicKeyUrl) {
+  const publicKeyCert = pki.certificateFromPem(cert);
+  if (!ca.cert) {
+    throw new Parse.Error(
+      Parse.Error.OBJECT_NOT_FOUND,
+      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
+    );
+  }
+  try {
+    if (!ca.cert.verify(publicKeyCert)) {
+      throw new Parse.Error(
+        Parse.Error.OBJECT_NOT_FOUND,
+        `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
+      );
+    }
+  } catch (e) {
+    throw new Parse.Error(
+      Parse.Error.OBJECT_NOT_FOUND,
+      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
+    );
+  }
+  return cert;
+}
+
 // Returns a promise that fulfills if this user id is valid.
 async function validateAuthData(authData) {
   if (!authData.id) {
     throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - authData id missing');
   }
@@ -124,13 +163,33 @@ async function validateAuthData(authData) {
   const publicKey = await getAppleCertificate(authData.publicKeyUrl);
   return verifySignature(publicKey, authData);
 }
 
 // Returns a promise that fulfills if this app id is valid.
-function validateAppId() {
-  return Promise.resolve();
+async function validateAppId(appIds, authData, options = {}) {
+  if (!options.rootCertificateUrl) {
+    options.rootCertificateUrl =
+      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
+  }
+  if (ca.url === options.rootCertificateUrl) {
+    return;
+  }
+  const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
+  if (
+    headers['content-type'] !== 'application/x-pem-file' ||
+    headers['content-length'] == null ||
+    headers['content-length'] > 10000
+  ) {
+    throw new Parse.Error(
+      Parse.Error.OBJECT_NOT_FOUND,
+      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
+    );
+  }
+  ca.cert = pki.certificateFromPem(certificate);
+  ca.url = options.rootCertificateUrl;
 }
 
 module.exports = {
   validateAppId,
   validateAuthData,
+  cache,
 };

Vulnerable code:             { from: "release", to: "alpha" },
      ['@semantic-release/github', {
        successComment: getReleaseComment(),
        labels: ['type:ci'],
        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
      }],


      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
      bundleId: 'cloud.xtralife.gamecenterauth',

    try {
      await gcenter.validateAuthData(authData);
      fail();
    } catch (e) {
      expect(e.message).toBe('Apple Game Center - invalid signature');
    }

  const headers = await new Promise((resolve, reject) =>
    headers['content-type'] !== 'application/pkix-cert' ||
    headers['content-length'] == null ||
    headers['content-length'] > 10000
      .get(publicKeyUrl, res => {
        let data = '';
          data += chunk.toString('base64');
          const cert = convertX509CertToPEM(data);
          if (res.headers['cache-control']) {
            var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
            if (expire) {
              cache[publicKeyUrl] = cert;
              // we'll expire the cache entry later, as per max-age
              setTimeout(() => {
                delete cache[publicKeyUrl];
              }, parseInt(expire[1], 10) * 1000);
            }
          resolve(cert);
function validateAppId() {
  return Promise.resolve();
Fixed code:       ['@semantic-release/github', {
        successComment: getReleaseComment(),
        labels: ['type:ci'],
        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
      }],
  const fs = require('fs');
  const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
  it('can load adapter', async () => {
    const options = {
      gcenter: {
        rootCertificateUrl:
          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
      },
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
  });
    const options = {
      gcenter: {
        rootCertificateUrl:
          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
      },
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      {}
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
      bundleId: 'com.example.com',
    await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
      new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
    );
    const options = {
      gcenter: {
        rootCertificateUrl:
          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
      },
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );

  it('should not validate Symantec Cert', async () => {
    const options = {
      gcenter: {
        rootCertificateUrl:
          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
      },
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
    expect(() =>
      gcenter.verifyPublicKeyIssuer(
        testCert,
        'https://static.gc.apple.com/public-key/gc-prod-4.cer'
      )
    );
  });

  it('adapter should load default cert', async () => {
    const options = {
      gcenter: {},
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
    const previous = new Date();
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );

    const duration = new Date().getTime() - previous.getTime();
    expect(duration).toEqual(0);
  });

  it('adapter should throw', async () => {
    const options = {
      gcenter: {
        rootCertificateUrl: 'https://example.com',
      },
    };
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      options
    );
    await expectAsync(
      adapter.validateAppId(
        appIds,
        { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
        providerOptions
      )
    ).toBeRejectedWith(
      new Parse.Error(
        Parse.Error.OBJECT_NOT_FOUND,
        'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
      )
    );
  });
-----BEGIN CERTIFICATE-----
MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/
MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd
BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj
IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa
Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w
DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM
IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7
ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA
17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz
ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT
q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC
ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k
LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv
bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw
IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE
SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC
hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV
woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu
tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz
vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ
tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr
EEbl0YdElmF8Hlamah/yNw==
-----END CERTIFICATE-----
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
  const cert_headers = await new Promise((resolve, reject) =>
  const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
    !validContentTypes.includes(cert_headers['content-type']) ||
    cert_headers['content-length'] == null ||
    cert_headers['content-length'] > 10000
  const { certificate, headers } = await getCertificate(publicKeyUrl);
  if (headers['cache-control']) {
    const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
    if (expire) {
      cache[publicKeyUrl] = certificate;
      // we'll expire the cache entry later, as per max-age
      setTimeout(() => {
        delete cache[publicKeyUrl];
      }, parseInt(expire[1], 10) * 1000);
    }
  }
  return verifyPublicKeyIssuer(certificate, publicKeyUrl);
}

function getCertificate(url, buffer) {
      .get(url, res => {
        const data = [];
          data.push(chunk);
          if (buffer) {
            resolve({ certificate: Buffer.concat(data), headers: res.headers });
            return;
          let cert = '';
          for (const chunk of data) {
            cert += chunk.toString('base64');
          }
          const certificate = convertX509CertToPEM(cert);
          resolve({ certificate, headers: res.headers });
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
  const publicKeyCert = pki.certificateFromPem(cert);
  if (!ca.cert) {
    throw new Parse.Error(
      Parse.Error.OBJECT_NOT_FOUND,
      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    );
  }
  try {
    if (!ca.cert.verify(publicKeyCert)) {
      throw new Parse.Error(
        Parse.Error.OBJECT_NOT_FOUND,
        `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
      );
    }
  } catch (e) {
    throw new Parse.Error(
      Parse.Error.OBJECT_NOT_FOUND,
      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    );
  }
  return cert;
}

async function validateAppId(appIds, authData, options = {}) {
  if (!options.rootCertificateUrl) {
    options.rootCertificateUrl =
      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
  }
  if (ca.url === options.rootCertificateUrl) {
    return;
  }
  const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
  if (
    headers['content-type'] !== 'application/x-pem-file' ||
    headers['content-length'] == null ||
    headers['content-length'] > 10000
  ) {
    throw new Parse.Error(
      Parse.Error.OBJECT_NOT_FOUND,
      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    );
  }
  ca.cert = pki.certificateFromPem(certificate);
  ca.url = options.rootCertificateUrl;
  cache,