Надежно проверьте цепочку сертификатов JWS и домен
Я пишу внутренний код для проверки JWS из Google SafetyNet API на Node.JS. Я был удивлен, что не нашел для этого готового модуля, поэтому я начал смотреть на простую проверку JWS с использованием доступных библиотек:
Прежде всего, Google заявляет, что необходимы следующие шаги:
- Извлеките цепочку сертификатов SSL из сообщения JWS.
- Проверьте цепочку сертификатов SSL и используйте сопоставление имен хостов SSL, чтобы убедиться, что конечный сертификат был выдан для имени хоста attest.android.com.
- Используйте сертификат для проверки подписи сообщения JWS.
- Проверьте данные сообщения 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')
}
Все статьи, которые мне помогли: