Подтвердить что ты не робот

Как использовать phpseclib для проверки того, что сертификат подписан публичным ЦС?

Мне нужно убедиться, что сертификат SMTP-сервера подписан публичным центром сертификации. Я хотел бы использовать phpseclib или другую доверенную библиотеку. Я считаю, что могу использовать корневые сертификаты, извлеченные из Firefox.

Здесь есть несколько подходов к проверке сертификатов и других метаданных, но не похоже, что любая проверка подписи как таковая (кроме обеспечения того, что OpenSSL делает это). В любом случае, я хочу использовать библиотеку - я хочу писать как можно меньше кода обработки сертификатов, так как я не криптограф.

Тем не менее ответы на вышеупомянутую ссылку по-прежнему очень полезны, так как это помогло мне получить код для получения сертификата из беседы TLS:

$url = "tcp://{$domain}:{$port}";
$connection_context_option = [
    'ssl' => [
        'capture_peer_cert' => true,
        'verify_peer' => false,
        'verify_peer_name' => false,
        'allow_self_signed' => true,
    ]
];
$connection_context = stream_context_create($connection_context_option);
$connection_client = stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $connection_context);
stream_set_timeout($connection_client, 2);
fread($connection_client, 10240);
fwrite($connection_client,"HELO alice\r\n");
fread($connection_client, 10240);
fwrite($connection_client, "STARTTLS\r\n");
fread($connection_client, 10240);
$ok = stream_socket_enable_crypto($connection_client, TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
if ($ok === false)
{
    return false;
}
$connection_info = stream_context_get_params($connection_client);

openssl_x509_export($info["options"]["ssl"]["peer_certificate"], $pem_encoded);

(Обратите внимание, что я отключил проверку сертификата здесь намеренно. Это связано с тем, что я не контролирую, на каких хостах это работает, и их сертификаты могут быть старыми или неправильно сконфигурированными. Поэтому я хочу получить сертификат независимо от проверки в соединении Я использую, а затем cacert.pem его сам, используя cacert.pem который я cacert.pem.)

Это даст мне такой сертификат. Это для почтового сервера Microsoft Live.com на smtp.live.com:587:

-----BEGIN CERTIFICATE-----
MIIG3TCCBcWgAwIBAgIQAtB7LVsRCmgbyWiiw7Sf5jANBgkqhkiG9w0BAQsFADBN
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E
aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcwOTEzMDAwMDAwWhcN
MTkwOTEzMTIwMDAwWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
aW9uMRQwEgYDVQQDEwtvdXRsb29rLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAIz2tovvgBmK4sOHgpyzCdtXrI0XOujctf6LHMj16wzUnMEatioS
tH0Pz0dKkCr/0yd9qtXbGhD1o6WhFsd7k651K9MZ98+uQ29SzTIAl6y1gkaBbp4h
MFXcE5EpRNHHmK8t2OR7hzmrvvNr6OTYv7BhVCw9pSrQqEFNno0K2TQRhAD9uzrL
OY+rBBVedCXWXH7uhZoZ6joUU7CEA5pPMzKPL1ro+Eorc8vt5FYOC+oAT587+b1M
z+jbZVQlq0qaMkBKRtUIII78MYY0n8DopGqHyzwqWoGySHJNC8256q+MwsZQvvQ3
vmy/rf61h2sg1tU0s7O88Yufxp0LSaMMzZcCAwEAAaOCA5owggOWMB8GA1UdIwQY
MBaAFA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBT7hLoZ/03rqwcslIc2
0k0z2R+vNTCCAdwGA1UdEQSCAdMwggHPggtvdXRsb29rLmNvbYIWKi5jbG8uZm9v
dHByaW50ZG5zLmNvbYIWKi5ucmIuZm9vdHByaW50ZG5zLmNvbYIgYXR0YWNobWVu
dC5vdXRsb29rLm9mZmljZXBwZS5uZXSCG2F0dGFjaG1lbnQub3V0bG9vay5saXZl
Lm5ldIIdYXR0YWNobWVudC5vdXRsb29rLm9mZmljZS5uZXSCHWNjcy5sb2dpbi5t
aWNyb3NvZnRvbmxpbmUuY29tgiFjY3Mtc2RmLmxvZ2luLm1pY3Jvc29mdG9ubGlu
ZS5jb22CC2hvdG1haWwuY29tgg0qLmhvdG1haWwuY29tggoqLmxpdmUuY29tghZt
YWlsLnNlcnZpY2VzLmxpdmUuY29tgg1vZmZpY2UzNjUuY29tgg8qLm9mZmljZTM2
NS5jb22CFyoub3V0bG9vay5vZmZpY2UzNjUuY29tgg0qLm91dGxvb2suY29tghYq
LmludGVybmFsLm91dGxvb2suY29tggwqLm9mZmljZS5jb22CEm91dGxvb2sub2Zm
aWNlLmNvbYIUc3Vic3RyYXRlLm9mZmljZS5jb22CGHN1YnN0cmF0ZS1zZGYub2Zm
aWNlLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMGsGA1UdHwRkMGIwL6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
bS9zc2NhLXNoYTItZzEuY3JsMC+gLaArhilodHRwOi8vY3JsNC5kaWdpY2VydC5j
b20vc3NjYS1zaGEyLWcxLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgG
CCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEC
AjB8BggrBgEFBQcBAQRwMG4wJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
ZXJ0LmNvbTBGBggrBgEFBQcwAoY6aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0U0hBMlNlY3VyZVNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMA0G
CSqGSIb3DQEBCwUAA4IBAQA3zjN7I6jTeL+08nhG5eAY0q4pLY40bCQHqONBLSI3
uRmQFUfrQOPYBqLC1QU+J2Z2HcX7YiqE3WAR3ODS9g2BAVXkKOQKNBnr2hKwueOz
qPwyvTyzcIQYUw+SrTX+bfJwYMTmZvtP9S7/pB1jPhrV7YGsD55AI9bGa9cmH7VQ
OiL1p5Qovg5KRsldoZeC04OF/UQIR1fv47VGptsHHGypvSo1JinJFQMXylqLIrUW
lV66p3Ui7pFABGc/Lv7nOyANXfLugBO8MyzydGA4NRGiS2MbGpswPCg154pWausU
M0qaEPsM2o3CSTfxSJQQIyEe+izV3UQqYSyWkNqCCFPN
-----END CERTIFICATE-----

Ок, отлично. Поэтому я хочу проверить это на любом публичном ЦС. Я считаю, что это действительный сертификат, и цепочка правильно проверена с помощью этой службы проверки:

Array
(
    [name] => /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
    [subject] => Array
        (
            [C] => US
            [ST] => Washington
            [L] => Redmond
            [O] => Microsoft Corporation
            [CN] => outlook.com
        )

    [hash] => a3c08ece
    [issuer] => Array
        (
            [C] => US
            [O] => DigiCert Inc
            [CN] => DigiCert SHA2 Secure Server CA
        )

    [version] => 2
    [serialNumber] => 3740952067977374966703603448215281638
    [serialNumberHex] => 02D07B2D5B110A681BC968A2C3B49FE6
    [validFrom] => 170913000000Z
    [validTo] => 190913120000Z
    [validFrom_time_t] => 1505260800
    [validTo_time_t] => 1568376000
    [signatureTypeSN] => RSA-SHA256
    [signatureTypeLN] => sha256WithRSAEncryption
    [signatureTypeNID] => 668
    [purposes] => Array
        (
            [1] => Array
                (
                    [0] => 1
                    [1] => 
                    [2] => sslclient
                )

            [2] => Array
                (
                    [0] => 1
                    [1] => 
                    [2] => sslserver
                )

            [3] => Array
                (
                    [0] => 1
                    [1] => 
                    [2] => nssslserver
                )

            [4] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => smimesign
                )

            [5] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => smimeencrypt
                )

            [6] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => crlsign
                )

            [7] => Array
                (
                    [0] => 1
                    [1] => 1
                    [2] => any
                )

            [8] => Array
                (
                    [0] => 1
                    [1] => 
                    [2] => ocsphelper
                )

            [9] => Array
                (
                    [0] => 
                    [1] => 
                    [2] => timestampsign
                )

        )

    [extensions] => Array
        (
            [authorityKeyIdentifier] => keyid:0F:80:61:1C:82:31:61:D5:2F:28:E7:8D:46:38:B4:2C:E1:C6:D9:E2

            [subjectKeyIdentifier] => FB:84:BA:19:FF:4D:EB:AB:07:2C:94:87:36:D2:4D:33:D9:1F:AF:35
            [subjectAltName] => DNS:outlook.com, DNS:*.clo.footprintdns.com, DNS:*.nrb.footprintdns.com, DNS:attachment.outlook.officeppe.net, DNS:attachment.outlook.live.net, DNS:attachment.outlook.office.net, DNS:ccs.login.microsoftonline.com, DNS:ccs-sdf.login.microsoftonline.com, DNS:hotmail.com, DNS:*.hotmail.com, DNS:*.live.com, DNS:mail.services.live.com, DNS:office365.com, DNS:*.office365.com, DNS:*.outlook.office365.com, DNS:*.outlook.com, DNS:*.internal.outlook.com, DNS:*.office.com, DNS:outlook.office.com, DNS:substrate.office.com, DNS:substrate-sdf.office.com
            [keyUsage] => Digital Signature, Key Encipherment
            [extendedKeyUsage] => TLS Web Server Authentication, TLS Web Client Authentication
            [crlDistributionPoints] => 
Full Name:
  URI:http://crl3.digicert.com/ssca-sha2-g1.crl

Full Name:
  URI:http://crl4.digicert.com/ssca-sha2-g1.crl

            [certificatePolicies] => Policy: 2.16.840.1.114412.1.1
  CPS: https://www.digicert.com/CPS
Policy: 2.23.140.1.2.2

            [authorityInfoAccess] => OCSP - URI:http://ocsp.digicert.com
CA Issuers - URI:http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt

            [basicConstraints] => CA:FALSE
        )

)

Вот как я пытаюсь проверить sig в phpseclib:

$x509 = new \phpseclib\File\X509();

// From the Mozilla bundle (getPublicCaCerts splits them with a regex)
$splitCerts = getPublicCaCerts(file_get_contents('cacert.pem'));

// Load the certs separately
$caStatus = true;
foreach ($splitCerts as $caCert)
{
    $caStatus = $caStatus && $x509->loadCA($caCert);
}
// $caStatus is now true, so all good here

$certData = $x509->loadX509($pem_encoded); // From the TLS server
$valid = $x509->validateSignature();
// $valid is now false

Это возвращает false, чего я не ожидаю. Интересно, правильно ли я получил исходные форматы? Загрузка ЦС и тестируемого сертификата, похоже, возвращает хорошие значения. К сожалению, документы phpseclib немного освещены примерами, и я не нашел многого в других местах в Интернете.

Кроме того: у меня есть неопределенное подозрение, что эта библиотека может мне помочь, если у нее есть функция проверки сертификата. Тем не менее, я думаю, что он пытается многое сделать для моего случая - я хочу, чтобы мое программное обеспечение запускалось на общем хостинге, а автоматическая загрузка воспринимается как другая движущаяся часть, которая может потерпеть неудачу. Я бы предпочел развернуть свой собственный пакет, предоставить общедоступную информацию CA как (большой) параметр и запустить проверку валидации in situ. phpseclib, вероятно, идеально подходит для этого, пока я могу определить форматы ввода!

Возможная причина: phpseclib не может найти соответствующий сертификат для тестирования

Я сузил проблему до цикла поиска в валидаторе phpseclib. На L2156 у нас есть этот код:

case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:

Константа действительно не определена, поэтому на самом деле тест состоит в том, может ли ЦС соответствовать правильному описанию сертификата. Сертификат имеет эти метаданные:

id-at-countryName = US
id-at-organizationName = DigiCert Inc
id-at-organizationalUnitName = www.digicert.com
id-at-commonName = DigiCert SHA2 High Assurance Server CA

И для всех текущих сертификатов, которые в противном случае совпадали бы, у меня есть только эти значения в последнем пакете cert (т.е. Все из приведенных ниже будет соответствовать, если бы не общее имя DigiCert SHA2 High Assurance Server CA не было найдено):

id-at-commonName = DigiCert Assured ID Root CA

id-at-commonName = DigiCert High Assurance EV Root CA

id-at-commonName = DigiCert Assured ID Root G2

id-at-commonName = DigiCert Assured ID Root G3

id-at-commonName = DigiCert Global Root G2

id-at-commonName = DigiCert Global Root G3

id-at-commonName = DigiCert Trusted Root G4

Таким образом, система даже не попадает далеко, как проверка цифровой подписи, поскольку она не может найти ЦС, соответствующий этому сертификату. Что мне не хватает? Эта простая задача должна быть намного проще!

Возможная причина: пакет Mozilla - это только сертификаты веб-сервера

Я предположил, что сертификаты почтовых серверов не входят в пакет Mozilla, потому что веб-браузер им не нужен. Я бы предположил, что сертификаты моей установки GNU/Linux Mint были бы современными и подходящими для этой цели, поскольку операционная система должна иметь возможность проверять сертификаты, используемые на почтовых серверах.

Поэтому я пробовал этот код, который загружает все системные сертификаты в phpseclib:

$certLocations = openssl_get_cert_locations();
$dir = $certLocations['default_cert_dir'];
$glob = $dir . '/*';
echo "Finding certs: " . $dir . "\n";

$x509 = new \phpseclib\File\X509();

foreach (glob($glob) as $certPath)
{
    // Change this so it is recursive?
    if (is_file($certPath))
    {
        $ok = $x509->loadCA(file_get_contents($certPath));
        if (!$ok)
        {
            echo sprintf("CA cert '%s' is invalid\n", $certPath);
        }
    }
}

// The 'getCertToTest' func just gets the live.com cert as a string
$data = $x509->loadX509(getCertToTest());
if (!$data)
{
    echo "Cert is invalid\n";
    exit();
}

$valid = $x509->validateSignature();
echo sprintf("Validation: %s\n", $valid ? 'Yes' : 'No');

К сожалению, это не удается.

Подтвердите, что мои сертификаты системы в порядке, используя openssl

Я выдал эту команду в своей системе, а удаленный сертификат TLS проверен ОК. Я плохо знаю код phpseclib, но это не похоже на то, что он выполняет цепочки, что, очевидно, необходимо.

openssl s_client -connect smtp.live.com:25 -starttls smtp
CONNECTED(00000003)
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Cloud Services CA-1
verify return:1
depth=0 C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = outlook.com
verify return:1
---
Certificate chain
 0 s:/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
   i:/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
 1 s:/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIG/jCCBeagAwIBAgIQDs2Q7J6KkeHe1d6ecU8P9DANBgkqhkiG9w0BAQsFADBL
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSUwIwYDVQQDExxE
aWdpQ2VydCBDbG91ZCBTZXJ2aWNlcyBDQS0xMB4XDTE3MDkxMzAwMDAwMFoXDTE4
MDkxMzEyMDAwMFowajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
(snipped, see other code block)
nGhseM2tJfwa2HMwUpuuo5029u4Dd40qvD0cMz33cOvBLRGkTPbXCFw24ZBdQrkt
SC5TAWzHFyT2tLC17LeSb7d0g+fuj41L6y4a9och8cPiv9IAP4sftzYupO99h4qg
7UXP7o3AOOGqrPS3INhO4068Z63indstanIHYM0IUHa3A2xrcz7ZbEuw1HiGH/Ba
HMz/gTSd2c0BXNiPeM7gdOK3
-----END CERTIFICATE-----
subject=/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
issuer=/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
---
No client certificate CA names sent
Client Certificate Types: RSA sign, DSA sign, ECDSA sign
Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Shared Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Peer signing digest: SHA1
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 3831 bytes and written 478 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: C11A0000050CD144CB5C49DD873D2C911F7CDDECFE18001F70FE0427C88B52F7
    Session-ID-ctx: 
    Master-Key: 5F4EC0B1198CF0A16D19F758E6A0961ED227FCEBD7EF96D4D6A7470E3F9B0453A2A06AC0C1691C31A1CA4B73209B38DE
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1519322480
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---
250 SMTPUTF8

Я могу отбросить phpseclib в пользу двоичной команды, но я буду полагаться на system/exec т.д., Которые могут быть недоступны. Тем не менее, работа иногда лучше, чем не работает всегда!

Резюме

Несмотря на обширную работу, я на этом зашел в тупик. Я приведу здесь то, что я хочу делать.

Я хочу использовать PHP для проверки SSL-сертификатов почтовых серверов в отношении известных публичных центров сертификации. Я не знаю, подходят ли для этого сертификаты Mozilla, или мне нужно получить их из других источников. Я обнаружил, что моя машина разработки Mint Linux имеет сертификаты, которые будут проверять пример почтового сервера выше.

Тривиальная стратегия здесь заключается в использовании PHP 5. 6+ и обеспечения включения всех параметров проверки в контексте потока (хотя в идеале я также хочу поддерживать 5.5). Тем не менее, я хочу сделать это самостоятельно, используя функции openssl_ или библиотеку, такую как phpseclib, поэтому я могу понять, почему данный сертификат действителен (или нет). Бинарный файл openssl делает это (как показано выше), и он делает это, по-видимому, используя что-то очень похожее на вызовы PHP openssl, но я не знаю, как это делается. Например, позволяет ли бинарная информация openssl использовать информацию о цепочке сертификатов?

Другой подход состоял бы в том, чтобы прочитать некоторую информацию с действительного сеанса SSL, но я не могу найти что-либо в руководстве, чтобы это сделать.

4b9b3361

Ответ 1

Сертификат подписывается промежуточным звеном, который в этом случае является CA-сервером DigiCert SHA2 Secure Server. Промежуточные сертификаты отсутствуют в списке корневых сертификатов. Независимо от того, какую библиотеку вы используете, я считаю, что вы должны явно предоставлять действительные промежуточные сертификаты для процесса проверки.

Вот пример использования библиотеки sop/x509.

// certificate from smtp.live.com
$cert = Certificate::fromPEM(PEM::fromString($certdata));
// list of trust anchors from https://curl.haxx.se/ca/cacert.pem
$trusted = CertificateBundle::fromPEMBundle(PEMBundle::fromFile('cacert.pem'));
// intermediate certificate from
// https://www.digicert.com/CACerts/DigiCertSHA2SecureServerCA.crt
$intermediates = new CertificateBundle(
    Certificate::fromDER(file_get_contents('DigiCertSHA2SecureServerCA.crt')));
// build certification path
$path_builder = new CertificationPathBuilder($trusted);
$certification_path = $path_builder->shortestPathToTarget($cert, $intermediates);
// validate certification path
$result = $certification_path->validate(PathValidationConfig::defaultConfig());
// failure would throw an exception
echo "Validation successful\n";

Это проверяет подпись и некоторые базовые проверки на RFC 5280. Он не проверяет соответствие CN или SAN целевому домену.

Отказ от ответственности! Я автор библиотеки. Это не доказано в битве, и поэтому я боюсь, что он не попадет в вашу категорию "некоторые другие доверенные библиотеки". Не стесняйтесь экспериментировать с ним, однако :).

Ответ 2

Я смог проверить его таким образом:

<?php
include('File/X509.php');

$certs = file_get_contents('cacert.pem');
$certs = preg_split('#==(?:=)+#', $certs);
foreach ($certs as &$cert) {
   $cert = trim(preg_replace('#-----END CERTIFICATE-----.+#s', '-----END CERTIFICATE-----', $cert));
}
unset($cert);
array_shift($certs);

$x509 = new File_X509();

foreach ($certs as $i => $cert) {
   $x509->loadCA($cert);
}

$test = file_get_contents('test.cer');

$x509->loadX509($test);
$opts = $x509->getExtension('id-pe-authorityInfoAccess');
foreach ($opts as $opt) {
    if ($opt['accessMethod'] == 'id-ad-caIssuers') {
        $url = $opt['accessLocation']['uniformResourceIdentifier'];
        break;
    }
}

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$intermediate = curl_exec($ch);

$x509->loadX509($intermediate);

if (!$x509->validateSignature()) {
    exit('validation failed');
}

$x509->loadCA($intermediate);

$x509->loadX509($test);

echo $x509->validateSignature() ?
    'good' :
    'bad';

Обратите внимание на $test = file_get_contents('test.cer'); немного. Это, когда я загрузил ваш сертификат. Если я прокомментировал $x509->loadCA($intermediate); сертификат не подтвердил. Если я оставлю его в нем, это подтвердится.

редактировать:

Эта ветка делает это автоматически:

https://github.com/terrafrost/phpseclib/tree/authority-info-access-1.0

Модульные тесты еще нужно добавить, однако это еще не в 2.0 или мастер-ветках. В этот уик-энд я постараюсь поработать над этим.

Пример использования:

<?php

include('File/X509.php');

$certs = file_get_contents('cacert.pem');
$certs = preg_split('#==(?:=)+#', $certs);
foreach ($certs as &$cert) {
   $cert = trim(preg_replace('#-----END CERTIFICATE-----.+#s', '-----END CERTIFICATE-----', $cert));
}
unset($cert);
array_shift($certs);

$x509 = new File_X509();

foreach ($certs as $i => $cert) {
   $x509->loadCA($cert);
}

$test = file_get_contents('test.cer');

$x509->loadX509($test);
//$x509->setRecurLimit(0);

echo $x509->validateSignature() ?
    'good' :
    'bad';

Ответ 3

Оказывается, я могу получить всю цепочку сертификатов с удаленного сервера - мне пришлось пройти через различные ложные выводы и хитроумные предположения, чтобы добраться до этого момента! Кредит Джо, который указал в комментариях, что параметр контекста capture_peer_cert получает сертификат сертификата без каких-либо сертификатов цепочки, которые завершат путь проверки к публичному ЦС; для этого требуется capture_peer_cert_chain.

Вот какой код для этого:

$url = "tcp://{$domain}:{$port}";
$connection_context_option = [
    'ssl' => [
        'capture_peer_cert_chain' => true,
        'verify_peer' => false,
        'verify_peer_name' => false,
        'allow_self_signed' => true,
    ]
];
$connection_context = stream_context_create($connection_context_option);
$connection_client = stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $connection_context);
// timeout fread after 2s
stream_set_timeout($connection_client, 2);
fread($connection_client, 10240);
fwrite($connection_client,"HELO alice\r\n");
// let the server introduce it self before sending command
fread($connection_client, 10240);
// send STARTTLS command
fwrite($connection_client, "STARTTLS\r\n");
// wait for server to say its ready, before switching
fread($connection_client, 10240);
// Switching to SSL/TLS
$ok = stream_socket_enable_crypto($connection_client, TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
if ($ok === false)
{
    return false;
}

$chainInfo = stream_context_get_params($connection_client);

Затем мы можем извлечь все сертификаты с помощью OpenSSL:

if (isset($chainInfo["options"]["ssl"]["peer_certificate_chain"]) && is_array($chainInfo["options"]["ssl"]["peer_certificate_chain"]))
{
    $verboseChainCerts = [];
    foreach ($chainInfo["options"]["ssl"]["peer_certificate_chain"] as $ord => $intermediate)
    {
        $chainCertOk = openssl_x509_export($intermediate, $verboseChainCerts[$ord]);
        if (!$chainCertOk)
        {
            $verboseChainCerts[$ord] = 'Cannot read chain info';
        }
    }

    $chainValid = checkChainAutomatically($x509Chain, $verboseChainCerts);
}

Наконец, функция, чтобы выполнить проверку, находится здесь. Вы должны предположить, что уже загружен хороший набор открытых сертификатов в соответствии с вопросом:

function checkChainAutomatically(X509 $x509, array $encodedCerts)
{
    // Set this to true as long as the loop will run
    $verified = (bool) $encodedCerts;

    // The certs should be tested in reverse order
    foreach (array_reverse($encodedCerts) as $certText)
    {
        $cert = $x509->loadX509($certText);
        $ok = $x509->validateSignature();
        if ($ok)
        {
            $x509->loadCA($cert);
        }
        $verified = $verified && $ok;
    }

    return $verified;
}

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