请教, 我有100多个ssl证书, 而且数量会变化, 作为https服务端怎么根据每个请求的域名不同, 使用不同的证书呢?

mgzhenhong

能否在 TcpConnection 的 stream_socket_enable_crypto 之前, 提供一个 beforeSslHandshake 回调方法来修改 socket 的 contentx, 来实现这个功能?

阅读 1447
4个回答

mgzhenhong

看了些资料, SSL在握手阶段, 客户端发的第一个Hello握手包里有域名, 要实现这个功能, 必须取得这个包里的Extension server_name数据. 现有的socket和stream函数好像没有这样的功能. 不知道是不是要用openssl自己实现ssl握手过程才行.

  • 暂无评论
six

区分域名的话看起来要openssl手动ssl握手了,这个难度有点大。

  • mgzhenhong 2019-11-19

    我看了TLS协议握手过程, 硬编码肯定可以实现, 但是效率存疑.
    况且这么大的东西要完全按照规范写到稳定健壮, 对我这个小项目就是杀鸡用牛刀了, 不太现实.

    下个项目会需要解析TLS握手包, 到时候再看.

mgzhenhong
$ctx = stream_context_create(["ssl" => [
    "local_cert" => "/path/to/cert.pem",
    "SNI_server_certs" => [
        "domain1.com" => "/path/to/domain1.pem",
        "*.domain2.com" => "/path/to/domain2.pem",
        "domain3.com" => "/path/to/domain3.pem"
    ]
]]);

找到一些资料, PHP 5.6以后, stream_socket_server 的 context 可以支持 SNI (Server Name Indication).
这样的话, 这个问题可以解决一半了, 服务端可以支持针对不同的域名使用不同的证书.

剩下的问题就是, 证书数量变化时, 在不重启 Server 的前提下, 如何平滑地重载 SNI 证书列表.

mgzhenhong

此问题已解决, 目前我这里运行良好. 关键代码和说明如下:

第一步: 声明 context, 启动服务.
$context 的 SNI_server_certs 部分留空, 但最终要将 SNI_server_certs 部分填充为注释所示的样子.

$context = [
    'ssl' => [
        'verify_peer'         => false,
        'disable_compression' => true,
        'SNI_enabled'         => true,
        'SNI_server_certs'    => [
            /*
            "*.domain1.com" => [
                'local_cert'          => "{$this->certFileRoot}/domain1.com/_.domain1.com.pem",
                'local_pk'            => "{$this->certFileRoot}/domain1.com/_.domain1.com.key",
            ],
            "*.domain2.com" => [
                'local_cert'          => "{$this->certFileRoot}/domain2.com/_.domain2.com.crt",
                'local_pk'            => "{$this->certFileRoot}/domain2.com/_.domain2.com.key",
            ],
            "domain3.com" => [
                'local_cert'          => "{$this->certFileRoot}/domain3.com/domain3.com.crt",
                'local_pk'            => "{$this->certFileRoot}/domain3.com/domain3.com.key",
            ],
            "www.domain3.com" => [
                'local_cert'          => "{$this->certFileRoot}/domain3.com/www.domain3.com.crt",
                'local_pk'            => "{$this->certFileRoot}/domain3.com/www.domain3.com.key",
            ],
            */
        ],
    ],
];

$server = new WorkerX("http://0.0.0.0:443", $context);
$server->count = 10;
$server->transport = 'ssl';
$server->name = 'Https Server';

第二步: 继承并重写 Worker 类, 以便于可以在运行时设置 stream_context
原Worker类中, 使用一个 protected 的 _context 属性保存socket上下文, 外部无法直接修改, 所以需要继承 Worker后,在我们实现的子类中修改.

另外, socket 上下文在php中是一个 resource 类型, 反映到php中可以视为内存地址引用. 对此变量的赋值操作不会创建新的对象.

class WorkerX extends \Workerman\Worker
{
    public function contextGetOptions()
    : array
    {
        if(is_resource($this->_context))
        {
            return stream_context_get_options($this->_context);
        }

        return [];
    }

    public function contextSetOptions(array $options)
    : bool
    {
        if(is_resource($this->_context))
        {
            return stream_context_set_option($this->_context, $options);
        }

        return false;
    }
}

第三步: 在$server的onWorkerStart回调中, 通过Channel注册事件, 允许外部通知服务器动态载入证书信息.
可以启动另外一个专用的api服务, api服务接收管理端的调用后, 发布 EVENT_REFRESH_CERT 事件, 事件数据中标明需要重载哪个站点的证书.

$server->onWorkerStart = function($worker)
{
    WorkerDI::init($worker);

    $this->refreshCert($worker, 0);

    \Channel\Client::on('EVENT_REFRESH_CERT', function($eventData) use ($worker)
    {
        $siteId = intval($eventData['site_id']);
        $this->refreshCert($worker, $siteId);
    });
};

private function refreshCert(WorkerX $worker, int $siteId = 0)
{
    $sslContextOptions = $worker->contextGetOptions();

    // 此处通过 $siteId 查询数据库, 取得站点绑定的域名, 和域名对应的证书文件路径
    $domain = '*.domain1.com';
    $certFilePath = '/path/to/certFile.crt';
    $keyFilePath = '/path/to/certFile.key';

    $sslContextOptions['ssl']['SNI_server_certs'][$domain] = [
        'local_cert' => $certFilePath,
        'local_pk'   => $pkFilePath,
    ];

    $setResult = $worker->contextSetOptions($sslContextOptions);

    echo "设置" . ($setResult ? "成功" : "失败") . "\n";
}

刷新页面, 此时证书已经在服务中生效, 功能完成.