如何使用 phpseclib 验证证书是否由 public CA 签名?
How to use phpseclib to verify that a certificate is signed by a public CA?
我需要确保 SMTP 服务器证书是由 public 证书颁发机构签署的。我想使用 phpseclib 或其他一些受信任的库。我相信我可以使用从 Firefox 中提取的 the root certificates。
有一些 home-brew approaches here 可以检查证书日期和其他元数据,但看起来不会像这样进行任何签名检查(除了确保 OpenSSL 会这样做)。无论如何,我想使用一个库——我想写尽可能少的证书处理代码,因为我不是密码学家。
也就是说,上面 link 的答案仍然非常有用,因为它帮助我获得了一些代码来从 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
自己验证它。)
那会给我这样的证书。这是用于 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-----
好的,很好。所以我想针对任何 public CA 验证这一点。我相信这是一个有效的证书,链已使用 this checking service:
正确验证
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
)
)
以下是我在 phpseclib 中验证 sig 的方法:
$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
这个returns false
,这不是我所期望的。我想知道我输入的格式是否正确? CA 的加载和被测证书似乎 return 很好的价值。不幸的是,phpseclib 文档对示例的介绍有点少,而且我在网络上的其他地方也找不到太多内容。
旁白:我有一个模糊的怀疑 this library 可以帮助我,假设它具有验证证书的功能。但是,我认为它正在尝试为我的情况做很多事情 - 我希望我的软件 运行 在共享主机上,自动下载感觉就像另一个可能会失败的移动部件。我宁愿部署我自己的包,提供 public CA 信息作为(大)参数,并 运行 验证测试 就地 。 phpseclib 可能是完美的,只要我能弄清楚输入格式!
可能原因:phpseclib 找不到匹配的证书进行测试
我已将问题缩小到 phpseclib 验证器中的 search loop。在 L2156 上,我们有这样的代码:
case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:
这个常量确实是未定义的,所以真正的测试是一个 CA 是否可以匹配正确的证书细节。该证书具有此元数据:
id-at-countryName = US
id-at-organizationName = DigiCert Inc
id-at-organizationalUnitName = www.digicert.com
id-at-commonName = DigiCert SHA2 High Assurance Server CA
对于所有本应匹配的当前证书,我在最新的证书包中只有这些值(即,如果不是因为未找到通用名称 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
因此,系统甚至无法进行数字签名检查,因为它找不到与该证书对应的 CA。我错过了什么?这个简单的任务应该比这容易很多!
可能的原因:Mozilla 包只是网络服务器证书
我推测邮件服务器证书不在 Mozilla 捆绑包中,因为 Web 浏览器不需要它们。我假设我的 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 证书被验证为 OK。我不太了解 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 根据已知的 public CA 验证邮件服务器 SSL 证书。我不知道 Mozilla 证书是否适合用于此目的,或者我是否需要从其他地方获取它们。我发现我的 Linux Mint 开发机器有证书可以验证上面的示例邮件服务器。
这里的简单策略是使用 PHP 5.6+ 并确保在流上下文中启用所有验证选项(尽管理想情况下,我希望也支持 5.5)。但是,我想自己做证明,要么使用 openssl_
函数,要么使用诸如 phpseclib 之类的库,这样我就可以看到为什么给定的证书有效(或无效)。 openssl
二进制文件执行此操作(如上所示)并且它可能使用与 PHP 的 openssl 调用非常相似的东西来执行此操作,但我不知道它是如何执行的。例如,openssl 二进制文件是否使用证书链信息来执行此操作?
另一种方法是从有效的 SSL 会话中读取一些信息,但我在手册中也找不到任何相关信息。
我是这样验证的:
<?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 或 master 分支中。这个周末我会努力做这件事。
使用示例:
<?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';
证书由中间人签署,在本例中为 DigiCert SHA2 Secure Server CA。根证书列表中不存在中间证书。无论您使用的是什么库,我相信您都必须为验证过程明确提供有效的中间证书。
这是一个使用 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 是否与目标域匹配。
免责声明!我是上述库的作者。它不是 battle-proven,因此恐怕它不会属于您的“其他一些受信任的图书馆”类别。不过,请随意尝试它:)。
事实证明我可以从远程服务器获取整个证书链 - 我不得不经历各种错误的线索和狡猾的假设才能达到这一点!感谢 Joe 在评论中指出,上下文选项 capture_peer_cert
仅获取证书 cert 而没有任何链证书可以完成 public CA 的验证路径;为此,需要 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);
}
最后,检查功能来了。根据问题,您应该假设已经加载了一组好的 public 证书:
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,然后继续外循环。可以这样做,直到确认列表中的所有证书都具有经过验证的签名。
我需要确保 SMTP 服务器证书是由 public 证书颁发机构签署的。我想使用 phpseclib 或其他一些受信任的库。我相信我可以使用从 Firefox 中提取的 the root certificates。
有一些 home-brew approaches here 可以检查证书日期和其他元数据,但看起来不会像这样进行任何签名检查(除了确保 OpenSSL 会这样做)。无论如何,我想使用一个库——我想写尽可能少的证书处理代码,因为我不是密码学家。
也就是说,上面 link 的答案仍然非常有用,因为它帮助我获得了一些代码来从 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
自己验证它。)
那会给我这样的证书。这是用于 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-----
好的,很好。所以我想针对任何 public CA 验证这一点。我相信这是一个有效的证书,链已使用 this checking service:
正确验证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
)
)
以下是我在 phpseclib 中验证 sig 的方法:
$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
这个returns false
,这不是我所期望的。我想知道我输入的格式是否正确? CA 的加载和被测证书似乎 return 很好的价值。不幸的是,phpseclib 文档对示例的介绍有点少,而且我在网络上的其他地方也找不到太多内容。
旁白:我有一个模糊的怀疑 this library 可以帮助我,假设它具有验证证书的功能。但是,我认为它正在尝试为我的情况做很多事情 - 我希望我的软件 运行 在共享主机上,自动下载感觉就像另一个可能会失败的移动部件。我宁愿部署我自己的包,提供 public CA 信息作为(大)参数,并 运行 验证测试 就地 。 phpseclib 可能是完美的,只要我能弄清楚输入格式!
可能原因:phpseclib 找不到匹配的证书进行测试
我已将问题缩小到 phpseclib 验证器中的 search loop。在 L2156 上,我们有这样的代码:
case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:
这个常量确实是未定义的,所以真正的测试是一个 CA 是否可以匹配正确的证书细节。该证书具有此元数据:
id-at-countryName = US
id-at-organizationName = DigiCert Inc
id-at-organizationalUnitName = www.digicert.com
id-at-commonName = DigiCert SHA2 High Assurance Server CA
对于所有本应匹配的当前证书,我在最新的证书包中只有这些值(即,如果不是因为未找到通用名称 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
因此,系统甚至无法进行数字签名检查,因为它找不到与该证书对应的 CA。我错过了什么?这个简单的任务应该比这容易很多!
可能的原因:Mozilla 包只是网络服务器证书
我推测邮件服务器证书不在 Mozilla 捆绑包中,因为 Web 浏览器不需要它们。我假设我的 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 证书被验证为 OK。我不太了解 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 根据已知的 public CA 验证邮件服务器 SSL 证书。我不知道 Mozilla 证书是否适合用于此目的,或者我是否需要从其他地方获取它们。我发现我的 Linux Mint 开发机器有证书可以验证上面的示例邮件服务器。
这里的简单策略是使用 PHP 5.6+ 并确保在流上下文中启用所有验证选项(尽管理想情况下,我希望也支持 5.5)。但是,我想自己做证明,要么使用 openssl_
函数,要么使用诸如 phpseclib 之类的库,这样我就可以看到为什么给定的证书有效(或无效)。 openssl
二进制文件执行此操作(如上所示)并且它可能使用与 PHP 的 openssl 调用非常相似的东西来执行此操作,但我不知道它是如何执行的。例如,openssl 二进制文件是否使用证书链信息来执行此操作?
另一种方法是从有效的 SSL 会话中读取一些信息,但我在手册中也找不到任何相关信息。
我是这样验证的:
<?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 或 master 分支中。这个周末我会努力做这件事。
使用示例:
<?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';
证书由中间人签署,在本例中为 DigiCert SHA2 Secure Server CA。根证书列表中不存在中间证书。无论您使用的是什么库,我相信您都必须为验证过程明确提供有效的中间证书。
这是一个使用 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 是否与目标域匹配。
免责声明!我是上述库的作者。它不是 battle-proven,因此恐怕它不会属于您的“其他一些受信任的图书馆”类别。不过,请随意尝试它:)。
事实证明我可以从远程服务器获取整个证书链 - 我不得不经历各种错误的线索和狡猾的假设才能达到这一点!感谢 Joe 在评论中指出,上下文选项 capture_peer_cert
仅获取证书 cert 而没有任何链证书可以完成 public CA 的验证路径;为此,需要 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);
}
最后,检查功能来了。根据问题,您应该假设已经加载了一组好的 public 证书:
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,然后继续外循环。可以这样做,直到确认列表中的所有证书都具有经过验证的签名。