Надежно проверьте цепочку сертификатов JWS и домен

Я пишу внутренний код для проверки JWS из Google SafetyNet API на Node.JS. Я был удивлен, что не нашел для этого готового модуля, поэтому я начал смотреть на простую проверку JWS с использованием доступных библиотек:

Прежде всего, Google заявляет, что необходимы следующие шаги:

  1. Извлеките цепочку сертификатов SSL из сообщения JWS.
  2. Проверьте цепочку сертификатов SSL и используйте сопоставление имен хостов SSL, чтобы убедиться, что конечный сертификат был выдан для имени хоста attest.android.com.
  3. Используйте сертификат для проверки подписи сообщения JWS.
  4. Проверьте данные сообщения JWS, чтобы убедиться, что они соответствуют данным в исходном запросе. В частности, убедитесь, что метка времени была проверена и что одноразовый номер, имя пакета и хэши сертификатов подписи приложения соответствуют ожидаемым значениям.

(из https://developer.android.com/training/safetynet/attestation)

Я нашел node-jose, который предлагал простой интерфейс для проверки JWS, и у него есть возможность разрешить встроенный ключ. Я пытаюсь понять, что именно делает этот процесс, и достаточно ли его для проверки подлинности JWS?

const {JWS} = require('node-jose');
const result = await JWS.createVerify({allowEmbeddedKey: true}).verify(jws);

if (result.key.kid === 'attest.android.com') {
  // Are we good to go or do we manually need to verify the certificate chain further?
}

Действительно ли использование встроенного ключа подтверждает цепочку встроенных сертификатов x5cиспользуя корневой ЦС, а подпись под сертификатом? Или мне нужно явно получить открытый ключ от Google для отдельной проверки сертификата?

Затем возникает несколько связанный с этим вопрос, касающийся API Google для выполнения этой проверки: есть API https://www.googleapis.com/androidcheck/v1/attestations/verify?key=...который выполняет эту точную операцию, но, похоже, он был удален из документации Google, и его можно найти только в устаревших статьях и ответах SO о SafetyNet, таких как этот, который, похоже, предполагает, что этот API предназначен только для тестирования и в производстве Вам следует выполнить проверку сертификата самостоятельно. Кто-нибудь знает, подходит ли этот API для производственного использования или нет? Если каждый предназначен для проверки JWS вручную, я нахожу немного удивительным, что Google не предлагает больше документации и примеров кода, поскольку этот процесс весьма подвержен ошибкам, а ошибки могут иметь серьезные последствия? Пока я нашел только несколько сторонних примеров на Java, но не нашел примеров серверного кода от Google.

1 ответ

Вот шаги, которые вам необходимо выполнить в соответствии с рекомендациями Google.

Определенно не стесняйтесь просматривать все ссылки, чтобы немного лучше понять процесс. Изучите все используемые здесь библиотечные функции, чтобы знать, что они делают, и если это именно то, что вы хотите, чтобы они делали. Я написал псевдокод, чтобы объяснить шаги. Возможно, вам придется запустить их на образце токена аттестации, чтобы проверить их и соответствующим образом изменить несколько вещей.

Также было бы хорошо рассмотреть всю реализацию узла SafetyNet в одном месте.

// following steps should be performed
// 1. decode the JWS
// 2. the source of the first certificate in x5c array of jws header 
//    should be attest.google.com
// 3. to make sure if the JWS was not tampered with, validate the signature of JWS (how signature verification is done is explained in the reference links)
//    with the certificate whose source we validated
// 4. if the signature was valid, we need to know if the certificate was valid by 
//    explicitly checking the certificate chain
// 5. Validate the payload by matching the package name, apkCertificateDigest
//    and nonce value (apkCertificateDigest is base64 encoding of the hash of signing app's certificate)
// 6. and now you can trust the ctsProfileMatch and BasicIntegrity flags
// let's see some code in node, though this will not run as-is, 
// it provides an outline on how to do it and which functions to consider when implementing

const pki = require('node-forge').pki;
const jws = require('jws');
const pem = require("pem");
const forge = require('node-forge');

const signedAttestation = "Your signed attestation here";

function deviceAttestationCheck(signedAttestation) {
  // 1. decode the jws
  const decodedJws = jws.decode(signedAttestation);
  const payload = JSON.parse(decodedJws.payload);

  // convert the certificate received in the x5c array into valid certificates by adding 
  // '-----BEGIN CERTIFICATE-----\n' and '-----END CERTIFICATE-----'
  // at the start and end respectively for each certificate in the array
  // and by adding '\n' at every 64 char
  // you'll have to write your own function to do the simple string reformatting
  // get the x5c certificate array
  const x5cArray = decodedJws.header.x5c;
  updatedX5cArray = doTheReformatting(x5cArray);

  // 2. verify the source to be attest.google.com
  certToVerify = updatedX5cArray[0];
  const details = pem.readCertificateInfo(certToVerify);
  // check if details.commanName === "attest.google.com"

  const certs = updatedX5cArray.map((cert) => pki.certificateFromPem(cert));

  // 3. Verify the signature with the certificate that we received
  // the first element of the certificate(certs array) is the one that was issued to us, so we should use that to verify the signature
  const isSignatureValid = jws.verify(signedAttestation, 'RS256', certs[0]);

  // 4. to be sure if the certificate we used to verify the signature is the valid one, we should validate the certificate chain
  const gsr2Reformatted = doTheReformatting(gsr2);
  const rootCert = pki.certificateFromPem(gsr2Reformatted);
  const caStore = pki.createCaStore([rootCert]);

  // NOTE: this pki implementation does not check for certificate revocation list, which is something that you'll need to do separately
  const isChainValid = pki.verifyCertificateChain(caStore, certs);

  // 5. now we can validate the payload
  // check the timestamps, to be within certain time say 1 hour
  // check nonce value, to contain the data that you expect, refer links below
  // check apkPackageName to be your app's package name
  // check apkCertificateDigestSha256 to be from your app - quick tip -look at the function below on how to generate this
  // finally you can trust the ctsProfileMatch - true/false depending on strict security need and basicIntegrity - true, minimum to check

}

// this function takes your signing certificate(should be of the form '----BEGIN CERT....data...---END CERT...') and converts into the SHA256 digest in hex, which looks like - 92:8H:N9:84:YT:94:8N.....
// we need to convert this hex digest to base64 
// 1. 92:8H:N9:84:YT:94:8N.....
// 2. 928hn984yt948n - remove the colon and toLowerCase
// 3. encode it in base64
function certificateToSha256DigestHex(certPem) {
  const cert = pki.certificateFromPem(certPem);
  const der = forge.asn1.toDer(pki.certificateToAsn1(cert)).getBytes();
  const m = forge.md.sha256.create();
  m.start();
  m.update(der);
  const fingerprint = m.digest()
      .toHex()
      .match(/.{2}/g)
      .join(':')
      .toUpperCase();

  return fingerprint
}

// 92:8H:N9:84:YT:94:8N => 928hn984yt948n
function stringToHex(sha256string) {
  return sha256string.split(":").join('').toLowerCase();
}

// this is what google sends you in apkCertificateDigestSha256 array
// 928hn984yt948n => "OIHf9wjfjkjf9fj0a="
function hexToBase64(hexString) {
  return Buffer.from(hexString, 'hex').toString('base64')
}

Все статьи, которые мне помогли:

  1. Краткое описание шагов - здесь
  2. подробное объяснение с реализацией - Здесь
  3. Что следует иметь в виду - здесь
  4. контрольный список от Google, чтобы сделать это правильно - Здесь
  5. Погрузитесь в процесс - здесь
Другие вопросы по тегам