前置知识
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()
实验过程
-
服务端使用
ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
在该模式下,启用的是
TLSv1.2
与TLSv1.3
两个安全的版本,客户端使用TLSv1.2
和TLSv1.3
连接都能成功,但是使用TLSv1.1
和TLSv1.0
版本会连接失败。失败时候会抛出如下异常。ssl.SSLError: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version
-
服务端使用
ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)
方式,即指定使用TLSv1.1
版本。客户端只能使用TLSv1.1
版本来连接,其他连接都会失败。
实验结论
通过上面实验,可以得知的结论是,如果服务端开始允许的TLS版本覆盖比较全,在后续迭代过程中,因为安全的考虑,禁用了某些版本TLS,将会直接导致采用被禁用版本TLS的客户端无法与服务端直接通信。且该过程发生在握手时候的认证过程。