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

时间:2008-09-16 16:41:50

标签: perl ssl https lwp

如何让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终于解决了这个问题。有关详细信息,请参阅my answer

8 个答案:

答案 0 :(得分:37)

这个长期存在的安全漏洞终于在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.(This bug已在LWP 6.03中修复。)为安全起见,请始终在verify_hostname => 1中指定ssl_opts

因此use LWP::UserAgent 6;应该足以验证服务器证书。

答案 1 :(得分:9)

根据您安装的SSL模块,有两种方法可以执行此操作。 LWP docs recommend installing Crypt::SSLeayCrypt::SSLeay docs。如果这就是您所做的,那么将HTTPS_CA_FILE环境变量设置为指向您的ca-bundle.crt应该可以解决问题。 ({{3}}提到了这一点,但对细节有点了解)。此外,根据您的设置,您可能需要设置HTTPS_CA_DIR环境变量。

Crypt :: SSLeay的示例:


use 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不会die,但会返回undef

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


use IO::Socket::SSL qw(debug3);
use 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"
    );
}
use LWP::Simple qw(get);

warn get("https:://some-server-with-bad-certificate.com");

此版本还会导致get()返回undef,但在执行时会向STDERR输出警告(如果从IO :: Socket导入调试*符号,则会发出一堆调试信息: :SSL):


% perl ssl_test.pl
DEBUG: .../IO/Socket/SSL.pm:1387: new ctx 139403496
DEBUG: .../IO/Socket/SSL.pm:269: socket not yet connected
DEBUG: .../IO/Socket/SSL.pm:271: socket connected
DEBUG: .../IO/Socket/SSL.pm:284: ssl handshake not started
DEBUG: .../IO/Socket/SSL.pm:327: Net::SSLeay::connect -> -1
DEBUG: .../IO/Socket/SSL.pm:1135: SSL connect attempt failed with unknown errorerror:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

DEBUG: .../IO/Socket/SSL.pm:333: fatal SSL error: SSL connect attempt failed with unknown errorerror:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
DEBUG: .../IO/Socket/SSL.pm:1422: free ctx 139403496 open=139403496
DEBUG: .../IO/Socket/SSL.pm:1425: OK free ctx 139403496
DEBUG: .../IO/Socket/SSL.pm:1135: IO::Socket::INET configuration failederror:00000000:lib(0):func(0):reason(0)
500 Can't connect to some-server-with-bad-certificate.com:443 (SSL connect attempt failed with unknown errorerror:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed) 

答案 2 :(得分:6)

我登陆此页面寻找绕过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。

答案 3 :(得分: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/

答案 4 :(得分:2)

此处提供的所有解决方案都存在一个主要的安全漏洞,因为它们只验证证书信任链的有效性,但不会将证书的公共名称与您要连接的主机名进行比较。因此,中间的人可以向您出示任意证书,只要LWP由您信任的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"
    );
}

答案 5 :(得分:1)

您也可以考虑Net :: SSLGlue(http://search.cpan.org/dist/Net-SSLGlue/lib/Net/SSLGlue.pm)但是,请注意,它取决于最近的IO :: Socket :: SSL和Net :: SSLeay版本。

答案 6 :(得分:1)

你是对此感到担忧的。不幸的是,我不认为在Perl看到的任何低级SSL / TLS绑定下100%安全地做到这一点。

基本上,您需要在握手开始之前传入要连接到SSL库的服务器的主机名。或者,您可以安排在适当的时候进行回调,如果没有签出,则从回调内部中止握手。编写Perl绑定到OpenSSL的人似乎一直在努力使回调接口一致。

根据服务器的证书检查主机名的方法也取决于协议。所以这必须是任何完美函数的参数。

您可能想查看是否存在与Netscape / Mozilla NSS库的任何绑定。当我看着它时,这似乎相当不错。

答案 7 :(得分:0)

只需在终端中执行以下命令: sudo cpan install Mozilla :: CA

它应该解决它。