数据的归宿有两存储:存储和传输。我们也可以认为,数据传输也是数据存储的一种形态,只是它的存储介质为网络。对于网络介质,如果有必要,我们也可以使用特殊的方法让数据持久化。如果数据的存储介质不是私有的,那么数据就暴露在不安全之中,尤其是网络这种特殊的数据存储介质。本文件写数据的网络介质中的安全编程。在具体的实战前,先讲讲安全相关概念。

数据加密

数据加密分为数据存储加密和数据传输加密。从加密方法来看,又可以分为单密钥加密,即对称加密和双密钥加密,即非对称加密。常见的对称加密算法有DES、AES、RC4等。非对称加密有著名的RSA公钥系统。

对称加密方法的私钥不能暴露,否则就能通过私钥解密被加密的数据。而在非对称加密中,公钥是可以公开的,私钥保密。任何人都可以使用RSA体系,通过把自己的信息使用公钥加密,然后发送给新人的公钥发布者,该发布者能通过私钥解密对方加密过的信息。

数字签名

数字签名是用于标记文件拥有者、发布者身份的字符串。通过数字签名可以标记文件的身份,所有者。目前使用可靠数字签名方法为非对称加密算法。

举一个例子。某公司发布以产品,安装文件为Product。该公司在Product中加入它们公司的数字签名。这个数字签名是该公司使用它们的私钥加密了Product文件的哈希值。当我们查看该文件的数字证书时,通过该公司公钥解密该哈希文件。这样就验证了Product是否被篡改。

PKI体系

PKI意为公钥基础设施,使用RSA加密体系。提供数字签名、加密、数字证书等服务。一般包括权威认证机构、证书作废系统、密钥备份、数字证书库等。如果企业需要数字证书,需要向认证机构申请,确保数字证书的安全。

Go实战

Go的标准库中的crypto提供了丰富的加密函数,我们直接使用它。

计算字符串的哈希值

哈希值通常用于校验文件的完整性和唯一性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"crypto/md5"
"crypto/sha1"
"fmt"
)

func main() {
name := []byte("I am Allenwind")
md := md5.New()
md.Write(name)
result := md.Sum([]byte(""))
fmt.Printf("md5 result is %x\n", result)

sha := sha1.New()
sha.Write(name)
result = sha.Sum([]byte(""))
fmt.Printf("sha1 result is %x\n", result)
}

计算文件的哈希

计算李健 - 传奇.mp3的哈希值。在写本文章时,这首歌已经在网易云音乐上下架了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"crypto/md5"
"crypto/sha1"
"fmt"
"io"
"log"
"os"
)

func main() {
file := "F:\\李健 - 传奇.mp3"
fd, err := os.Open(file)
if err != nil {
log.Fatal(err)
}

md := md5.New()
io.Copy(md, fd)
result := md.Sum([]byte(""))
fmt.Printf("file %s md5 result is %x\n", file, result)

sha := sha1.New()
io.Copy(sha, fd)
result = sha.Sum([]byte(""))
fmt.Printf("file %s sha1 result is %x\n", file, result)
}

计算大文件的哈希值

Go中的io.Copy是文件流过程,不用特殊的处理就可以计算大文件的散列而又不用一次加载到内存中。对于Python就需要一点小技巧。

1
2
3
4
5
6
7
8
9
10
import functools
import hashlib

def hexdigest(path):
md5 = hashlib.md5()
with open(path, "rb") as fd:
ifd = iter(functools.partial(fd.read, 1024**2), b'')
for data in ifd:
md5.update(data)
print(md5.hexdigest())

HTTPS编程

在HTTP的基础上添加SSL(Secure Sockets Layer)就是我们所说的HTTPS。从协议栈的角度看,加密层位于HTTP和TCP之间。建立HTTPS连接的大致过程如下:

(1)浏览器或客户端主动请求服务器
(2)服务器向浏览器或客户端返回证书,让浏览器或客户端检查证书的合法性
(3)证书中包括发行者、有效期信息、指纹(如上述的MD5、SHA1)、RSA非对称加密的公钥
(4)浏览器随机挑选一对称加密方法(如AES),并使用公钥加密所选的加密方法,接着使用对称加密方法把这次请求的内容加密,加密后把请求内容和加密过的公钥发给服务器
(5)服务器使用私钥解密浏览器的请求内容和使用的对称加密方法
(6)服务器根据对方的请求内容和使用的加密方法,对返回的内容加密后发送给浏览器
(7)上述方法就是一次加密通信大概过程

HTTPS编程需要证书和密钥,证书一般由受信任的证书颁发机构颁发。使用如下命令生成:

1
openssl req -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem

然后根据提示创建即可。

具体效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$openssl req -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem
Generating a 2048 bit RSA private key
.............................................+++
..+++
writing new private key to 'server_key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

生成的两个文件”server_cert.pem”, “server_key.pem”,在Go中的http.ListenAndServeTLS会使用。

第一个HTTPS程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"net/http"
)

var message string = "Hello, https"

func secureHandler(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Type", "text/html")
resp.Header().Set("Content-Length", fmt.Sprint(len(message)))
resp.Write([]byte(message))
}

func main() {
http.HandleFunc("/https", secureHandler)
http.ListenAndServeTLS(":8080", "server_cert.pem", "server_key.pem", nil)
}

来一个实用点的程序,支持HTTPS的文件服务器

1
2
3
4
5
6
7
8
9
10
package main

import (
"net/http"
)

func main() {
handler := http.FileServer(http.Dir("F:"))
http.ListenAndServeTLS(":8080", "server_cert.pem", "server_key.pem", handler)
}

Python实战

本节包含散列计算和sockets、HTTPS。

计算文件的哈希值

这里以计算linuxmint-18.3-cinnamon-64bit.iso(最新版的linux mint)做例子。出于性能考虑,通常使用读取固定大小块的方式计算散列而不是把文件;一次加载到内存。Python中有一种Pythonic的做法,通过迭代器可以避免使用条件语句来判断文件指针是否到达结尾。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import functools
import hashlib

def hexdigest(path, trunk=1024**2):
md5 = hashlib.md5()
with open(path, "rb") as fd:
ifd = iter(functools.partial(fd.read, trunk), b'')
for data in ifd:
md5.update(data)
return md5.hexdigest()

def main():
path = r'D:\software\linuxmint-18.3-cinnamon-64bit.iso'
md5_hex = hexdigest(path)
print(md5_hex)

if __name__ == '__main__':
main()

我们也可以使用lambda表达式代替functools.partial。即:
iter(lambda: fd.read(trunk), b'')

为socket添加SSL支持

Python标准库ssl模块可以为socket连接提供SSL支持,通过ssl.wrap_socket()函数接受一个socket作为参数并用SSL层来包装它。下面演示简单的例子。创建证书的方法上面提及。

服务器端的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import socket
import ssl

key_file = 'server_key.pem' # 服务器的私钥
cert_file = 'server_cert.pem' # 发送给客户端的证书

def echo_client(s):
while True:
data = s.recv(8192)
if not data:
break
s.send(data)
s.close()
print('connection closed')

def echo_server(address):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(address)
s.listen(5)

s_ssl = ssl.wrap_socket(s,
keyfile=key_file,
certfile=cert_file,
server_side=True)
while True:
try:
sock, address = s_ssl.accept()
print('connection from', address, sock)
echo_client(sock)
except Exception as err:
print(err)

if __name__ == '__main__':
echo_server(('', 8080))

客户端的实现:

1
2
3
4
5
6
7
8
def ssl_client(address):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_c = ssl.wrap_socket(s,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs='server_cert.pem')
ssl_c.connect(address)
ssl_c.send(b'hello ssl socket')
print(ssl_c.recv(8192)

为HTTP添加SSL支持

HTTP建立在TCP上,通过上述的方法,为TCP添加安全层就相当于为HTTP添加安全层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import http.server

class SSLMixin:
def __init__(self, *args, keyfile=None, certfile=None, ca_certs=None,
cert_reqs=ssl.CERT_REQUIRED, **kwargs):
self._keyfile = keyfile
self._certfile = certfile
self._ca_certs = ca_certs
self._cert_reqs = cert_reqs
super().__init__(*args, **kwargs)

def get_request(self):
client, addr = super().get_request()
client_ssl = ssl.wrap_socket(client,
keyfile=self._keyfile,
certfile=self._certfile,
ca_certs=self._ca_certs,
cert_reqs=self._cert_reqs,
server_side=True)
return client_ssl, addr

class SSLHTTPServer(SSLMixin, http.server.HTTPServer):
pass

类似地,xmlrpc.server.SimpleXMLRPCServer也可以做大概的处理。

转载请包括本文地址:https://allenwind.github.io/blog/4758
更多文章请参考:https://allenwind.github.io/blog/archives/