我该如何让LWP验证SSL服务器证书?

49
我该如何让LWP验证我正在连接的服务器的证书是由受信任的机构签署并发放给正确的主机?据我所知,它甚至不检查证书是否声称为我正在连接的主机名。这似乎是一个重大的安全漏洞(特别是最近的DNS漏洞)。
更新:事实证明我真正想要的是HTTPS_CA_DIR,因为我没有ca-bundle.crt。但是HTTPS_CA_DIR=/usr/share/ca-certificates/解决了问题。无论如何,我仍将标记答案为已接受,因为它已经足够接近了。
更新2:事实证明,如果您使用Net::SSL作为基础SSL库,则HTTPS_CA_DIRHTTPS_CA_FILE仅适用。但是LWP也可以使用IO::Socket::SSL,后者将忽略这些环境变量,并愉快地与任何服务器通信,无论它提供什么证书。有更一般的解决方案吗?
更新3:不幸的是,解决方案仍然不完整。 Net::SSL和IO::Socket::SSL都没有检查主机名与证书的匹配。这意味着某人可以为某个域获取合法证书,然后冒充任何其他域而不会引起LWP的投诉。

更新 4:LWP 6.00 最终解决了这个问题。请参阅我的答案以获取详细信息。

8个回答

41

libwww-perl的6.00版本中,这个长期存在的安全漏洞终于得到了修复。从那个版本开始,默认情况下LWP::UserAgent会验证HTTPS服务器是否提供了与预期主机名匹配的有效证书(除非将$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME}设置为false值,或者为了向后兼容如果该变量没有设置,则设置$ENV{HTTPS_CA_FILE}$ENV{HTTPS_CA_DIR})。

这可以通过LWP::UserAgent的新选项ssl_opts进行控制。请查看该链接以了解如何定位证书颁发机构证书的详细信息。但是要小心,原来的LWP::UserAgent的工作方式是,如果您在构造函数中提供了一个ssl_opts哈希,则verify_hostname默认为0而不是1。(这个错误已经在LWP 6.03中修复)。为了安全起见,请总是在ssl_opts中指定verify_hostname => 1

因此,use LWP::UserAgent 6;就足以使服务器证书得到验证。


9

根据您安装的SSL模块不同,有两种方法可以实现此操作。LWP文档建议安装Crypt :: SSLeay。如果您已经这样做了,则将 HTTPS_CA_FILE 环境变量设置为指向您的ca-bundle.crt即可解决问题。( Crypt :: SSLeay文档提到了这一点,但细节比较少)。此外,根据您的设置,您可能需要设置 HTTPS_CA_DIR 环境变量。

Crypt :: SSLeay示例:


使用 LWP::Simple qw(get) 模块下载网页内容;
$ENV{HTTPS_CA_FILE} = "/path/to/your/ca/file/ca-bundle";
$ENV{HTTPS_DEBUG} = 1;

print get("https://some-server-with-bad-certificate.com");
__END__ SSL_connect:before/connect initialization SSL_connect:SSLv2/v3 write client hello A SSL_connect:SSLv3 read server hello A SSL3 alert write:fatal:unknown CA SSL_connect:error in SSLv3 read server certificate B SSL_connect:error in SSLv3 read server certificate B SSL_connect:before/connect initialization SSL_connect:SSLv3 write client hello A SSL_connect:SSLv3 read server hello A SSL3 alert write:fatal:bad certificate SSL_connect:error in SSLv3 read server certificate B SSL_connect:before/connect initialization SSL_connect:SSLv2 write client hello A SSL_connect:error in SSLv2 read server hello B

请注意,get函数不会抛出异常,但会返回undef。

或者,您可以使用IO::Socket::SSL模块(也可从CPAN获取)。要验证服务器证书,需要修改SSL上下文默认值:


使用 IO::Socket::SSL qw(debug3);
使用 Net::SSLeay;
BEGIN {
    IO::Socket::SSL::set_ctx_defaults(
        verify_mode => Net::SSLeay->VERIFY_PEER(),
        ca_file => "/path/to/ca-bundle.crt",
      # ca_path => "/alternate/path/to/cert/authority/directory"
    );
}
使用 LWP::Simple qw(get);
警告:get("https:://some-server-with-bad-certificate.com")将返回undef,但会在执行时打印到STDERR(以及如果从IO::Socket::SSL导入debug*符号,则会打印一堆调试信息):

此版本还会导致get()在执行时返回undef,但会将警告打印到STDERR(以及如果您从IO::Socket::SSL中导入debug*符号,则会打印大量调试信息):


% perl ssl_test.pl
DEBUG: .../IO/Socket/SSL.pm:1387: 新的 ctx 139403496
DEBUG: .../IO/Socket/SSL.pm:269: 套接字尚未连接
DEBUG: .../IO/Socket/SSL.pm:271: 套接字已连接
DEBUG: .../IO/Socket/SSL.pm:284: SSL 握手未开始
DEBUG: .../IO/Socket/SSL.pm:327: Net::SSLeay::connect -> -1
DEBUG: .../IO/Socket/SSL.pm:1135: SSL 连接尝试失败,原因为未知错误error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
DEBUG: .../IO/Socket/SSL.pm:333: 致命的 SSL 错误:SSL 连接尝试失败,原因为未知错误error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed DEBUG: .../IO/Socket/SSL.pm:1422: 释放 ctx 139403496 open=139403496 DEBUG: .../IO/Socket/SSL.pm:1425: OK 释放 ctx 139403496 DEBUG: .../IO/Socket/SSL.pm:1135: IO::Socket::INET 配置失败error:00000000:lib(0):func(0):reason(0) 500 无法连接到 some-server-with-bad-certificate.com:443 (SSL 连接尝试失败,原因为未知错误error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed)

7

我来到这个页面是为了寻找绕过SSL验证的方法,但所有的答案仍然非常有帮助。以下是我的发现。对于那些寻求绕过SSL验证的人(不建议,但可能会有一些情况下你必须这样做),我使用的是lwp 6.05,以下内容对我有效:

use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request::Common qw(GET);
use Net::SSL;

my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 }, );
my $req = GET 'https://github.com';
my $res = $ua->request($req);
if ($res->is_success) {
    print $res->content;
} else {
    print $res->status_line . "\n";
}

我也在一个使用POST的页面上进行了测试,并且测试成功。关键是要使用Net::SSL并将verify_hostname = 0。


在古老、不支持的内部网络硬件上获胜。 - Stickley

2
以下所有解决方案都存在一个主要的安全漏洞,即它们仅验证证书信任链的有效性,但不将证书的公共名称与您连接到的主机名进行比较。因此,中间人可以向您呈现任意证书,并且只要它由您信任的CA签名,LWP就会愉快地接受它。虚假证书的公共名称是无关紧要的,因为LWP从未检查它。
如果您使用IO::Socket::SSL作为LWP的后端,则可以通过设置verifycn_scheme参数来启用对公共名称的验证,如下所示:
use IO::Socket::SSL;
use Net::SSLeay;
BEGIN {
    IO::Socket::SSL::set_ctx_defaults(
        verify_mode => Net::SSLeay->VERIFY_PEER(),
        verifycn_scheme => 'http',
        ca_path => "/etc/ssl/certs"
    );
}

2
不,被接受的解决方案并没有遇到这个问题。(好吧,我自己写的。)LWP 6默认会将通用名称与主机名进行比较,如果它们不匹配,则会中止。(您是正确的,之前的LWP版本没有这样做。) - cjm
1
这不正确,我正在使用最新版本的LWP :: UserAgent(版本6.04)作为SOAP :: Lite(版本0.714)的后端。 LWP :: UserAgent的后端在此机器上是IO :: Socket :: SSL。我发现,如果没有包含上面的代码,则既不检查CN也不验证证书链。使用ssl_opts()设置“verify_hostname”和“SSL_ca_path”没有任何效果。 - blumentopf
1
我敢打赌你已经设置了$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME}$ENV{HTTPS_CA_FILE}$ENV{HTTPS_CA_DIR}中的任何一个,这些都可以禁用主机名检查。 - cjm
1
不,这些既不是设置在我的环境中,也没有被SOAP::Lite触及。你真的测试过你的解决方案吗?使用了哪些后端?具体来说,你是否使用IO::Socket::SSL进行了测试? - blumentopf
1
我没有尝试过使用SOAP::Lite,但是我确实尝试过使用IO::Socket::SSL。(事实上,只有 IO::Socket::SSL 可以进行主机名验证;Net::SSL 无法执行该操作。也许这就是你的问题;如果你已经加载了 Net::SSL,则 LWP 可能会使用它而不是 IO::Socket::SSL。手动加载 IO::Socket::SSL 将使情况反转。)lwp-request -m HEAD https://host1 可以正常工作。 lwp-request -m HEAD https://host1-alt 失败,因为名称不匹配。PERL_LWP_SSL_VERIFY_HOSTNAME=0 lwp-request -m HEAD https://host1-alt 可以正常工作。 - cjm

2
如果您直接使用LWP::UserAgent(而不是通过LWP::Simple),您可以通过向HTTP :: Request对象添加“If-SSL-Cert-Subject”头来验证证书中的主机名。该头的值被视为要应用于证书主题的正则表达式,如果不匹配,则请求失败。例如:
#!/usr/bin/perl 
use LWP::UserAgent;
my $ua = LWP::UserAgent->new();
my $req = HTTP::Request->new(GET => 'https://yourdomain.tld/whatever');
$req->header('If-SSL-Cert-Subject' => '/CN=make-it-fail.tld');

my $res = $ua->request( $req );

print "Status: " . $res->status_line . "\n"

将会打印

Status: 500 Bad SSL certificate subject: '/C=CA/ST=Ontario/L=Ottawa/O=Your Org/CN=yourdomain.tld' !~ //CN=make-it-fail.tld/

1

1

你对此感到担忧是正确的。不幸的是,我认为在我查看的任何低级别的Perl SSL/TLS绑定下,都不可能以100%的安全性进行。

基本上,在握手开始之前,您需要将要连接到SSL库的服务器的主机名传递给它。或者,您可以安排在正确时刻发生回调,并在回调内部中止握手,如果它不符合要求的话。编写Perl绑定到OpenSSL的人似乎在使回调接口一致方面遇到了麻烦。

检查主机名与服务器证书是否匹配的方法也取决于协议。因此,这将成为任何完美函数的参数。

您可能想看看是否有绑定到Netscape/Mozilla NSS库的绑定。当我看过它时,它似乎非常擅长做到这一点。


0
只需在终端执行以下命令即可: sudo cpan install Mozilla::CA 它应该可以解决问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接