在python使用SSL(HTTPS)

在python上使用SSL有许多场景,我主要关注的是使用python访问HTTPS资源,以及使用python提供HTTPS服务。(HTTPS是SSL在WEB上的应用之一)

一、使用python访问HTTPS网站

这应该算是最简单也是最常见的场景了。我们使用python做为客户端去访问公网上的网站,而这个网站为了传输安全(避免被劫持或者窃听)使用了HTTPS服务,传输过程内容都经过了SSL加密。下面来看下具体的python代码,这里使用的是python2.7.11,用的是python自带的urllib2。当然你也可以使用requests或者pycurl等第三方库,都可以,原理都是一样的,只是实现的手段有些差异而已。

import urllib2

import ssl

if __name__ == '__main__':

    myurl="https://www.baidu.com"

    req = urllib2.Request(myurl)

    try:

        response = urllib2.urlopen(req)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error :%s" % str(ex)

运行它,我们就可以得到这个网站上的内容了。并且传输过程都经过了加密。是不是很简单。整个的传输过程大概是这样的,客户端请求SSL连接握手(其中会跟服务器有些SSL协议之间的交互,这个我们不用详细去研究)服务器把自己的证书传给客户端,客户端对这个证书进行认证(每台客户端电脑上都会默认安装一些权威CA(证书签发机构)的证书,这个认证就是使用这些证书进行的,python的ssl模块会自动加载这些权威CA证书,当然你也可以自己指定,这个后面会谈到),确保这个证书是由可信的CA颁布的(客户端对服务端证书进行认证这个操作,是从python2.7.9开始才有的,以前版本的是不进行认证的),之后就是利用证书交换密钥后,使用密钥对传输内容进行加密传输。

互联网上大部分的正规网站都是会有自己的证书的,这些证书都是经过权威CA认证的,可以被认为是可信的(其实这个也不一定,前段时间就有过谷歌封杀赛门铁克CA签发的证书的事件,有兴趣的可以去网上搜索看看)。但是同样也有不少网站,由于各种各样的原因没有这些经过权威CA认证的证书,毕竟申请这些证书流程比较麻烦,并且要支付一定的费用。这些网站也可以提供安全的HTTPS服务,但由于他们的证书是自签名的或者是由非权威的CA颁发的(这两种证书后面会谈到),所以会被认为是不可信的。当使用浏览器打开这些网站时,浏览器会提醒你这是不安全的连接,当然这个不安全是指网站未经权威认证,所以网站本身可能不安全,但在网络传输层面上来看,传输是安全的。那我们访问这些网站时,使用上面的代码会有错误提示,下面代码就是访问一个这是我自己建立的一个HTTPS网站https://127.0.0.1:8443,该网站使用的是自签名的证书。如果你使用谷歌浏览器访问会提示你:您的连接不是私密连接,如果你强制连接则会提示不安全

import urllib2

import ssl

if __name__ == '__main__':

    myurl="https://127.0.0.1:8443"

    req = urllib2.Request(myurl)

    try:

        response = urllib2.urlopen(req)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error :%s" % str(ex)

运行这个就会出现错误:

提示证书校验错误。这时如果我们想要访问这样的网站就要把客户端的证书校验关闭。在前面加上一句:ssl._create_default_https_context = ssl._create_unverified_context即可

import urllib2

import ssl

ssl._create_default_https_context = ssl._create_unverified_context

if __name__ == '__main__':

    myurl="https://127.0.0.1:8443"

    req = urllib2.Request(myurl)

    try:

        response = urllib2.urlopen(req)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error :%s" % str(ex)

当然也可以自己创建一个不校验的SSL上下文,然后引用这个上下文来打开url

ctx = ssl._create_unverified_context()

然后

response = urllib2.urlopen(req,context=ctx)

 

二、使用OPENSSL生成证书

1、生成自签名的证书

在使用python提供HTTPS服务之前,我们需要先生成证书,这里请参考https://www.cnblogs.com/wucao/archive/2017/02/27/6474711.html 这篇文章讲解了如何使用openssl生成证书。在WINDOWS平台记得要设置环境变量,参考我的一篇博文http://blog.sina.com.cn/s/blog_5d18f85f0102xdm7.html。

使用命令:openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout private.pem -out cert.crt

之后会要求我们输入一些组织信息,你可以根据你的实际情况填写。之后就生成了自签名的证书cert.crt,私钥是private.pem

2、自己建立一个CA(证书签发机构),然后用自己的CA来颁发证书。这个我们后面讨论双向认证的时候会用到。详细内容请参考http://blog.csdn.net/gx_1983/article/details/47866537

这里我只是简单的写一下我自己的步骤

1)      保证openssl的bin目录在path环境变量里面。创建一个工作目录,这里我使用ca这个目录。然后在ca下面再创建目录demoCA。之后在demoCA下创建空白文本文件index.txt和serial,并且打开serial写入字符01

2)      先造成CA的KEY和证书

openssl req -new -x509 -days 36500 -key ca.key -out ca.crt

执行后会出现提示,主要是要求输入一些组织方面的信息,请按要求填写即可

执行成功后会造成ca.key和ca.crt 两个文件,ca.key为私钥需要妥善保管,不要轻易给别人。ca.crt为证书可以随意传播。

3)      生成服务器的私钥和证书,其中证书使用了CA的私钥进行签名:

l  生成server私钥

openssl genrsa -out server.key 2048

l  使用server私钥生成server端证书请求文件

openssl req -new -key server.key -out server.csr

一样需要回答一些组织方面的问题

l  使用server证书请求文件通过CA生成由CA签名的证书

openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key

l  验证server证书

openssl verify -CAfile ca.crt server.crt

这样就得到两个文件:一个是私钥server.key;一个是证书server.crt

4)      用跟3)同样的方法生成客户端的client.key和client.crt这两个文件在后面的双向认证里面会用到

三、使用python提供HTTPS服务

我们有了证书和私钥了,下面就可以正式使用python建立一个HTTPS网站了。这里我使用框架实现,用的是twisted。使用的证书是之前用OPENSSL生成的自签名证书。

#-* -coding: utf-8 -* -

from twisted.web import server, resource

from twisted.internet import reactor,ssl

class MainResource(resource.Resource):

    isLeaf = True

    # 用于处理GET类型请求

    def render_GET(self, request):

        # name参数

        name = 'World'

        if request.args.has_key('name'):

            name = request.args['name'][0]

        # 设置响应编码

        request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")

        # 响应的内容直接返回

        return "

Hello, " + name + ""

if __name__ == '__main__':

    sslContext = ssl.DefaultOpenSSLContextFactory(

        'C:/ca/private.pem, # 私钥

        'C:/ca/cert.crt' # 证书

    )

    site = server.Site(MainResource())

    reactor.listenSSL(8080, site, sslContext)

    print "监听端口:8080"

reactor.run()

使用之前的客户端访问,记得关闭证书校验。这时可以看到可以顺利访问。如果使用浏览器访问,也可以正常访问,但此时会出现安全提示,忽略这个安全提示后也可以正常访问。

四、SSL双向校验

上面的示例都是客户端对服务器端证书的校验。这也是最常见的单向校验。这里面由客户端来校验服务器端的证书(当然也可以选择不校验)。通常的互联网服务都是这种方式。在这种方式下,客户端不需要有证书。服务器端也不会校验客户端的证书。此外还有一种双向校验模式。在这种模式下,客户端也要有证书,并且在协商过程中提供给服务器端。服务器也会对客户端的证书进行校验。这种模式一般应用于对安全要求比较高的环境,服务器和用户端都需要证书,并且双方都要能被权威CA验证通过。看看下面的服务器端代码:

#-* -coding: utf-8 -* -

from twisted.web import server, resource

from twisted.internet import reactor,ssl

from OpenSSL import SSL

def verifyCallback(connection, x509, errnum, errdepth, ok):

    if not ok:

        print 'invalid cert from subject:', x509.get_subject()

        return False

    else:

        print "Certs are fine", x509.get_subject()

    return True

class MainResource(resource.Resource):

    isLeaf = True

    # 用于处理GET类型请求

    def render_GET(self, request):

        # name参数

        name = 'World'

        if request.args.has_key('name'):

            name = request.args['name'][0]

        # 设置响应编码

        request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")

        # 响应的内容直接返回

        return "

Hello, " + name + ""

if __name__ == '__main__':

    sslContext = ssl.DefaultOpenSSLContextFactory(

        'C:/ca/private.pem, # 私钥

        'C:/ca/cert.crt' # 证书

    )

    ctx = sslContext.getContext()

   

#这里改为了双向校验模式

    ctx.set_verify(

        SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,

        verifyCallback

        )

    site = server.Site(MainResource())

    reactor.listenSSL(8080, site, sslContext)

    print "监听端口:8080"

reactor.run()

然后再用客户端来访问一下。这时会发现有错误出现,错误提示为:

提示sslv3协议握手失败。原因就是服务器端要求双向校验,但客户端没有提供自己的证书给服务器,所以握手协商失败。

好的,我们再改一下客户端代码,适配这种双向校验模式。这里客户端使用使用的证书是,是之前使用OPENSSL生成的客户端证书,并且使用了我们自己建立的CA进行签名。

import urllib2

import ssl

KEY_FILE="C:/ca/client.key"

CERT_FILE="C:/ca/client.crt"

context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 

context.check_hostname = False 

context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)

context.verify_mode=ssl.CERT_REQUIRED

       

if __name__ == '__main__':

    myurl="https://127.0.0.1:8080/?name=qh"

    req = urllib2.Request(myurl)

    try:

        response = urllib2.urlopen(req,context=context)

        #response = urllib2.urlopen(req)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error in auth phase:%s" % str(ex)

这里出现错误提示

证书校验失败。这是因为服务器提供的证书没有通过客户端的校验。

这是因为服务器端的证书是使用自签名的证书,当然通不过客户端校验了。这里我们要把服务器端的证书改成我们使用CA签名的证书。修改一下服务器端代码:

    sslContext = ssl.DefaultOpenSSLContextFactory(

        'C:/ca/server.key', # 私钥

        'C:/ca/server.crt' # 证书

    )

改过之后再试,可以还出现同样的错误,这是怎么回事呢?原来服务器端的证书是使用我们自建的CA进行签名的,不是权威CA,所以校验失败了。这怎么办呢,我们可以指定CA的证书,把我们的CA证书设置为可信。修改一下客户端,加上一句:

CA_FILE="c:/ca/ca.crt"

context.load_verify_locations(CA_FILE)

注意ca.crt是之前我们使用OPENSSL建立的CA证书(请参考前面第二部分的内容)。修改之后重新运行,发现还是出现错误:

这个错误提示变了,提示CA不知名。同时在服务器端的回调函数也有错误输出:invalid cert from subject:

这说明服务器端校验客户端证书失败,原因当然是服务器端我们没有把我们自己的CA设置为可信。所以同样的在服务器端也要修改一下,加上以下两句:

cafile="c:/ca/ca.crt"

ctx.load_verify_locations( cafile)

重新运行,这下成功了

下面把完整的代码发一下:

服务器端:

#-* -coding: utf-8 -* -

'''

Created on 2018-1-16

@author: qh

'''

from twisted.internet import iocpreactor as iocpreactor

try:

    iocpreactor.install()

except Exception, e:

    print "iocp install failed:%s" % str(e)

   

from twisted.web import server, resource

from twisted.internet import reactor,ssl

from OpenSSL import SSL

cafile="c:/ca/ca.crt"

class MainResource(resource.Resource):

    isLeaf = True

    # 用于处理GET类型请求

    def render_GET(self, request):

        # name参数

        name = 'World'

        if request.args.has_key('name'):

            name = request.args['name'][0]

        # 设置响应编码

        request.responseHeaders.addRawHeader("Content-Type", "text/html; charset=utf-8")

        # 响应的内容直接返回

        return "

Hello, " + name + ""

if __name__ == '__main__':

    sslContext = ssl.DefaultOpenSSLContextFactory(

        'C:/ca/server.key', # 私钥

        'C:/ca/server.crt' # 证书

    )

    ctx = sslContext.getContext()

   

    ctx.set_verify(

        SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,

        verifyCallback

        )

    ctx.load_verify_locations( cafile)

    site = server.Site(MainResource())

    reactor.listenSSL(8080, site, sslContext)

    print "监听端口:8080"

    reactor.run()

客户端:

'''

Created on 2018-3-2

@author: qh

'''

import urllib2

import ssl

KEY_FILE="C:/ca/client.key"

CERT_FILE="C:/ca/client.crt"

CA_FILE="c:/ca/ca.crt"

context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 

context.check_hostname = False 

context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)

context.load_verify_locations(CA_FILE)

context.verify_mode=ssl.CERT_REQUIRED

if __name__ == '__main__':

    req = urllib2.Request(myurl)

    try:

        response = urllib2.urlopen(req,context=context)

        #response = urllib2.urlopen(req)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error in auth phase:%s" % str(ex)

五、其它

1、context.load_cert_chain(certfile=CERT_FILE,keyfile=KEY_FILE)

这个函数可以只提供一个certfile这一个文件,这时要求CERT_FILE里面包括私钥。其实证书文件和私钥文件都是简单的文本文件,你可以把私钥文件的内容复制到证书文件后面,这样你就可以在这个函数里面只提供一个文件了。

2、如何在request对象里面读出客户端信息呢?

所有的客户端信息都可以request对象里面读出,包括客户端的证书信息,看看下面,你所需要的基本都有了

def render_GET(self, request):

        print request.content.read()

        print request.getAllHeaders()

        print request.getClientIP()

        print request.getHost()

        print request.transport.getPeer()

        cl=request.transport.getPeerCertificate()

        certificate = ssl.Certificate(cl)

        print certificate.getIssuer()

        print certificate.getSubject()

3、使用POST方法发送JSON数据

def urllib2_open(myurl):

    headers = {'Content-Type': 'application/json'}

    data={'test':'hello'}

    req = urllib2.Request(myurl,headers=headers, data=json.dumps(data))

    try:

       

        response = urllib2.urlopen(req,context=context)

        print "HTTP return code:%d" % response.getcode()

        strResult= response.read()

        print strResult

    except Exception ,ex:

        print "Found Error in auth phase:%s" % str(ex)

4、有一个地方我研究的不是特别清楚:默认情况下python是到哪里找权威CA证书的,是在某个文件夹还在通过注册表,还是直接通过操作系统提供的API。

写这么多总算把该写的写完了。之前我只是想用python访问一个HTTPS站点的。但当时很不顺利,不管怎么搞都出现SSLV3_ALERT_HANDSHAKE_FAILURE,当时在网上找了很多类似的解决办法,什么启用SSLV3啊、把加密方式改为ALL啊,另外也用了requests和pycurl等等第三方组件。但所有尝试无一成功。花了我好几天时间。最后终于发现原来这个站点需要双向认证。从这里面感觉自己对SSL了解太少了,走了太多弯路。于是就对SSL好好研究了一下,这才有了上面的内容。这些内容还是比较肤浅,都只限于应用方面,但对我来说足够了。分享给大家,让大家少走我走过的弯路。

猜你喜欢

转载自www.cnblogs.com/jiangzhaowei/p/9123467.html