【Python】深入理解TLS协议(附Python实现)

文章详细介绍了TLS/SSL协议如何利用散列函数、对称加密和非对称加密确保数据安全传输。通过非对称加密进行身份验证和密钥协商,对称加密处理数据加密,散列函数验证信息完整性。文中还提供了使用Python生成测试证书和实现TLS认证的脚本,以及客户端和服务端的示例代码,强调了不同TLS版本兼容性对通信的影响。
摘要由CSDN通过智能技术生成

前置知识

TLS/SSL构成

TLS/SSL的功能实现主要依赖于三类基本算法:散列(哈希)函数 Hash对称加密非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列(哈希)函数验证信息的完整性。
在这里插入图片描述
可以理解为,TLS/SSL干了两件事,其一是保证传输内容不被篡改,最常用保证内容不被篡改的方法就是使用散列算法,以MD5为例,通过对传输内容计算MD5值,通过对比MD5值来验证接收数据是否完整。只有MD5只能保障数据传输完整,但不能保障别人无法看到内容,所以需要对传输的数据进行加密,这就是第二件事。

加密算法

对称加密

使用一个密码对内容进行加密和解密。常见的有DES,AES,IDEA算法。

加密:密文=加密算法(明文,密码)

解密:明文=解密算法(密文,密码)

关键点在于加密和解密使用的同一密码,且加密/解密区别于对应算法的两种不同模式。需要注意的是,在安全领域中,该算法中的密码保存本身就是一个不安全的问题。

非对称加密

非对称加密算法指的是加、解密使用不同的密钥,一把为公开的公钥,另一把为私钥。公钥加密的内容只能由私钥进行解密,反之由私钥加密的内容只能由公钥进行解密。也就是说,这一对公钥、私钥都可以用来加密和解密,并且一方加密的内容只能由对方进行解密。

加密:公钥加密,私钥解密的过程,称为「加密」

签名:私钥加密,公钥解密的过程,称为「签名」

非对称加密例子:让A写下一个任意3位数,并将这个数和91相乘;然后将积的最后三位数告诉B,这样B就可以计算出A写下的是什么数字了。

加密传输:A写下的是123 (明文),并且A计算出123 * 91 (B的公钥加密)等于11193,将结果的末三位193(加密结果)传给B;

解密过程:B把接收的193再乘以11 (B的私钥),193 * 11(解密过程) = 2123 末三位123(明文)就是A需要传输的内容;

原理:91乘以11等于1001,而任何一个三位数乘以1001后,末三位显然都不变。

使用Python实现TLS认证

生成证书脚本

# Generate the certificates and keys for testing.


PROJECT_NAME="www.test.com"

# Generate the openssl configuration files.
cat > ca_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no

[ req_distinguished_name ]
 O                      = $PROJECT_NAME Dodgy Certificate Authority
EOF

cat > server_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no

[ req_distinguished_name ]
 O                      = $PROJECT_NAME
 CN                     = 127.0.0.1
EOF

cat > client_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no

[ req_distinguished_name ]
 O                      = $PROJECT_NAME Device Certificate
 CN                     = 127.0.0.1
EOF

mkdir ca
mkdir server
mkdir client
mkdir certDER

# private key generation
openssl genrsa -out ca.key 2048
openssl genrsa -out server.key 2048
openssl genrsa -out client.key 2048

# cert requests
openssl req -out ca.req -key ca.key -new -config ./ca_cert.conf
openssl req -out server.req -key server.key -new -config ./server_cert.conf
openssl req -out client.req -key client.key -new -config ./client_cert.conf

# generate the actual certs.
openssl x509 -req -in ca.req -out ca.crt -sha1 -days 5000 -signkey ca.key
openssl x509 -req -in server.req -out server.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key
openssl x509 -req -in client.req -out client.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key

openssl x509 -in ca.crt -outform DER -out ca.der
openssl x509 -in server.crt -outform DER -out server.der
openssl x509 -in client.crt -outform DER -out client.der

mv ca.crt ca.key ca/
mv server.crt server.key server/
mv client.crt client.key client/

mv ca.der server.der client.der certDER/

rm *.conf
rm *.req
rm *.srl

服务端脚本

# 此代码仅用于分析握手阶段,后续异常请忽略
import socket
import ssl

def run_server():
    # context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context = ssl.SSLContext(ssl.PROTOCOL_TLS)
    context.set_ciphers('ALL:@SECLEVEL=0')
    # context.options = ssl.OP_NO_TLSv1_2
    
    context.load_cert_chain(certfile="server/server.crt", keyfile="server/server.key")
    context.load_verify_locations(cafile="ca/ca.crt")
    context.verify_mode = ssl.CERT_REQUIRED
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
        sock.bind(("127.0.0.1", 1443))
        sock.listen(5)
        with context.wrap_socket(sock, server_side=True) as ssock:
            conn, addr = ssock.accept()
            with conn:
                print("Connected by", addr)
                while True:
                    data = conn.recv(1024)
                    if not data:
                        break
                    conn.sendall(data)

if __name__ == '__main__':
    run_server()


客户端脚本

import socket
import ssl


def connect_to_server():
    hostname = '127.0.0.1'
    # context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
    # context = ssl.SSLContext(ssl.PROTOCOL_TLS)
    # context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
    context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)
    context.set_ciphers('ALL:@SECLEVEL=0')
    context.check_hostname = False
    context.load_cert_chain(certfile="client/client.crt", keyfile="client/client.key")
    context.load_verify_locations("ca/ca.crt")
    context.verify_mode = ssl.CERT_REQUIRED
    with socket.create_connection((hostname, 1443)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            print(ssock.version())


if __name__ == '__main__':
    connect_to_server()
使用urllib3的客户端脚本
import ssl
import urllib3
from requests.packages.urllib3.util import ssl_

def urllib_req():
    SSL_OPTIONS = ssl.OP_NO_TLSv1_1
   
    ctx = ssl_.create_urllib3_context(ssl.PROTOCOL_TLS)
    ctx.options |= SSL_OPTIONS
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
    ctx.set_ciphers('ALL:@SECLEVEL=0')
    ctx.check_hostname = False
    ctx.load_cert_chain(certfile="client/client.crt", keyfile="client/client.key")
    ctx.load_verify_locations("ca/ca.crt")
    http = urllib3.PoolManager(
        num_pools=1,
        maxsize=1,
        block=1,
        ssl_context=ctx,
    )
    resp = http.request('GET', 'https://127.0.0.1:1443')
    print(resp.status)


if __name__ == '__main__':
    urllib_req()

实验过程

  1. 服务端使用ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

    在该模式下,启用的是TLSv1.2TLSv1.3两个安全的版本,客户端使用TLSv1.2TLSv1.3连接都能成功,但是使用TLSv1.1TLSv1.0版本会连接失败。失败时候会抛出如下异常。

    ssl.SSLError: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version
    
  2. 服务端使用ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)方式,即指定使用TLSv1.1版本。客户端只能使用TLSv1.1版本来连接,其他连接都会失败。

实验结论

通过上面实验,可以得知的结论是,如果服务端开始允许的TLS版本覆盖比较全,在后续迭代过程中,因为安全的考虑,禁用了某些版本TLS,将会直接导致采用被禁用版本TLS的客户端无法与服务端直接通信。且该过程发生在握手时候的认证过程。

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值