简介
尽管互联网最初被设计为可以承受敌对代理攻击的系统,但它过去一直是在一个可信的实体和合作的环境中发展起来的。可惜现在已经时过境迁。垃圾邮件,拒绝服务攻击,网络钓鱼这些东西使得每一个上网者都需要自行承担风险。
应用程序应当在复杂的互联网环境中仍然可以正确工作。“正确”不光意味着程序功能的正确,同时还意味着要确保数据传输过程中的保密性和完整性,甚至只允许合法用户进行访问和其它问题。
这自然使得编程更加复杂。在构建安全应用程序的过程中,会出现很复杂和微妙的问题。如果你想自己这样做(如实现一个自有安全库),通常都会以失败而告终。相反,你需要使用安全专家设计的安全库。
ISO 安全架构
功能层次
主要的安全系统功能
- 认证 - 身份校验文件
- 数据完整性 - 数据不被篡改
- 保密 - 数据不能暴露给他人
- 公证/签名
- 访问控制
- 保证/可用性
必须的 OSI 协议栈
- 对等实体认证 (3, 4, 7)
- 数据源认证 (3, 4, 7)
- 访问控制服务 (3, 4, 7)
- 连接保密 (1, 2, 3, 4, 6, 7)
- 无连接的保密 (1, 2, 3, 4, 6, 7)
- 选择性字段的保密 (6, 7)
- 传输保密 (1, 3, 7)
- 恢复连接的完整性 (4, 7)
- 不可恢复连接的完整性 (4, 7)
- 选择字段连接完整性 (7)
- 选择字段的无连接完整性 (7)
- 接受源 (7)
- 接受回执 (7)
机制
- 对等实体认证
o 加密
o 数字签名
o 交换验证 - 数据源认证
o 加密
o 数字签名 - 访问控制服务
o 访问控制列表
o 密码
o 范围列表
o 等级 - 连接保密
o 密码
o 路由控制 - 无连接的保密
o 密码
o 路由控制 - 选择性字段的保密
o 密码 - 传输保密
o 密码
o 传输填充
o 路由控制 - 恢复连接的完整性
o 密码
o 数据完整性 - 不可恢复连接的完整性
o 密码
o 数据完整性 - 选择字段连接完整性
o 密码
o 数据完整性 - 无连接完整性
o 密码
o 数字签名
o 数据完整性 - 选择字段的无连接完整性
o 密码
o 数字签名
o 数据完整性 - 接受源
o 数字签名
o 数据完整性
o 公正 - 接受回执
o 数字签名
o 数据完整性
o 公正
数据完整性
确保数据的完整性意味着要提供一个数据未被篡改的测试方法。通常是通过字节数据生成一个简单的数字。这个操作被称为hashing
,结果数字称为 hash
或者 hash 值
。
有一个幼稚的 hash 算法是将数据所有的字节进行总和。然而,这却仍然允许数据在保留 hash值不变的情况下对数据进行任意改变。例如,攻击者只需要交换两个字节。这样 hash 值没有改变,但结果可能是你本来欠别人 256 美元却变成了 65535 美元。
用于安全目的的 hash 算法必须很“强”,这样攻击者才很难在保留相同的 hash 值时找到一个不同的字节序列。这样攻击者才很难修改数据以达到目的。安全研究人员不断测试是否能攻破 hash 算法 - 寻找一个简单方法,得到一个字节序列来匹配某个 hash 值。他们设计了一系列被认为很强的加密 hash 算法.
Go 支持几个 hash 算法,包括 MD4
, MD5
, RIPEMD-160
, SHA1
, SHA224
, SHA256
, SHA384
andSHA512
。它们都尽可能按照 Go 程序员关注的,遵循相同的模式:在适当的包中定义 New或类似的方法,返回一个 hash 包中的 Hash 对象。
一个 Hash 结构体拥有一个io.Writer 接口
,你可以通过 writer 方法写入被 hash 的数据.你可以通过 Size
方法获取 hash 值的长度,Sum
方法返回 hash 值。
MD5 算法是个典型的例子。使用md5 包
,hash 值是一个 16 位的数组。通常以 ASCII 形式输出四个由 4 字节组成的十六进制数。程序如下
/* MD5Hash
*/
package main
import (
"crypto/md5"
"fmt"
)
func main() {
hash := md5.New()
bytes := []byte("hello\n")
hash.Write(bytes)
hashValue := hash.Sum(nil)
hashSize := hash.Size()
for n := 0; n < hashSize; n += 4 {
var val uint32
val = uint32(hashValue[n])<<24
uint32(hashValue[n+1])<<16 +
uint32(hashValue[n+2])<<8 +
uint32(hashValue[n+3])
fmt.Printf("%x ", val)
}
fmt.Println()
}
输出 b1946ac9 2492d234 7c6235b4 d2611184
在此基础上的一个变化是 HMAC(Keyed-Hash Message Authentication Code)
,它给 hash 算法增加了一个 key。使用时略有不同。要和 key 一起使用 MD5 算法时,可以通过以下形式替换New
func NewMD5(key []byte) hash.Hash
key 对称加密
数据加密有两种机制。第一种方式是在加密和解密时都使用同一个 key。加密方和解密方都需要知道这个 key。此处如何在这两者之间传输这个 key。
目前有很多使用 hash 算法的加密算法。其中很多都有弱点,而且随着时间的推移,计算机越来越快,通用 hash 算法变得越来越弱。Go 已经支持好几个对称加密算法,如 Blowfish
和DES
。
这些算法都是 block 算法
。因为它们必须基于数据块(block)。如果你的数据不匹配 block 的大小,那就必须在最后使用空格来填充多余的空间。
每个算法都被表示为一个 Cipher 对象
。可通过在相应的包中使用对称 key 作为参数调用NewCipher 方法
来创建该对象。
创建 cipher 对象后,就能通过它加密和解密数据块。Blowfish 需要 8 位的 block,详见以下程序
/* Blowfish
*/
package main
import (
"bytes"
"golang.org/x/crypto/blowfish"
"fmt"
)
func main() {
//创建Cipher对象
key := []byte("my key")
cipher, err := blowfish.NewCipher(key)
if err != nil {
fmt.Println(err.Error())
}
src := []byte("hello\n\n\n")
//加密
var enc [512]byte
cipher.Encrypt(enc[0:], src)
//解密
var decrypt [8]byte
cipher.Decrypt(decrypt[0:], enc[0:])
result := bytes.NewBuffer(nil)
result.Write(decrypt[0:8])
fmt.Println(string(result.Bytes()))
}
Blowfish 不在 GO 1 中,而是在golang.org/x/
中。你可以在需要使用它的源码目录下运行“go get”进行安装。
这里跟书上不一致,书上那个路径无法安装
公钥加密
公钥
加密和解密需要两个 key
:一个用来加密,另一个用来解密。加密 key 通常是公开的,这样任何人都可以给你发送加密数据。解密 key 必须保密,否则任何人都可以解密数据。公钥系统是非对称
的,不同的 key 有不同的用途。
Go 支持很多公钥加密系统,RSA
就是一个典型的例子。
下面是一个生成 RSA 公钥和私钥的程序
/* GenRSAKeys
*/
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/gob"
"encoding/pem"
"fmt"
"os"
)
func main() {
reader := rand.Reader
bitSize := 512
key, err := rsa.GenerateKey(reader, bitSize)
checkError(err)
fmt.Println("Private key primes", key.Primes[0].String(), key.Primes[1].String())
fmt.Println("Private key exponent", key.D.String())
publicKey := key.PublicKey
fmt.Println("Public key modulus", publicKey.N.String())
fmt.Println("Public key exponent", publicKey.E)
saveGobKey("private.key", key)
saveGobKey("public.key", publicKey)
savePEMKey("private.pem", key)
}
func saveGobKey(fileName string, key interface{}) {
outFile, err := os.Create(fileName)
checkError(err)
encoder:= gob.NewEncoder(outFile)
err = encoder.Encode(key)
checkError(err)
outFile.Close()
}
func savePEMKey(fileName string, key *rsa.PrivateKey) {
outFile, err := os.Create(fileName)
checkError(err)
var privateKey = &pem.Block{Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key)}
pem.Encode(outFile, privateKey)
outFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
结果:
Private key primes 110447466565545308568164154050664723270540238389548895971109016198476127576859 98755224833317938999382105964603507460774905292171139250410857315173921679329
Private key exponent 459510764742620794664451847126349205280233225231873265875659066190282785510957283637157196363689167630079521868520347867735685374968042398806722557092545
Public key modulus 10907264392950792835901550418369991983502587787765765384169890699352612428117415829901964302612687784931700029077242737891886577847347776552469164799047611
Public key exponent 65537
程序通过 gob 序列化保存证书。也可以取回证书:
/* LoadRSAKeys
*/
package main
import (
"crypto/rsa"
"encoding/gob"
"fmt"
"os"
)
func main() {
var key rsa.PrivateKey
loadKey("private.key", &key)
fmt.Println("Private key primes", key.Primes[0].String(), key.Primes[1].String())
fmt.Println("Private key exponent", key.D.String())
var publicKey rsa.PublicKey
loadKey("public.key", &publicKey)
fmt.Println("Public key modulus", publicKey.N.String())
fmt.Println("Public key exponent", publicKey.E)
}
func loadKey(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := gob.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
X.509 证书
公钥基础架构(PKI)是一个公钥集合框架,它连同附加信息,如所有者名称和位置,以及它们之间的联系提供了一些审批机制。
目前主要使用的 PKI 是就是基于 X.509 证书
的。例如浏览器使用它验证站点的身份。
下面的程序是为自己的站点生成自签名 X.509 证书并保存到一个.cer 文件中
/* GenX509Cert
*/
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/gob"
"encoding/pem"
"fmt"
"math/big"
"os"
"time"
)
func main() {
random := rand.Reader
var key rsa.PrivateKey
loadKey("private.key", &key)
now := time.Now()
then := now.Add(60 * 60 * 24 * 365 * 1000 * 1000 * 1000) // one year
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "jan.newmarch.name",
Organization: []string{"Jan Newmarch"},
},
// NotBefore: time.Unix(now, 0).UTC(),
// NotAfter: time.Unix(now+60*60*24*365, 0).UTC(),
NotBefore: now,
NotAfter: then,
SubjectKeyId: []byte{1, 2, 3, 4},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
DNSNames: []string{"jan.newmarch.name", "localhost"},
}
derBytes, err := x509.CreateCertificate(random, &template,
&template, &key.PublicKey, &key)
checkError(err)
certCerFile, err := os.Create("jan.newmarch.name.cer")
checkError(err)
certCerFile.Write(derBytes)
certCerFile.Close()
certPEMFile, err := os.Create("jan.newmarch.name.pem")
checkError(err)
pem.Encode(certPEMFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certPEMFile.Close()
keyPEMFile, err := os.Create("private.pem")
checkError(err)
pem.Encode(keyPEMFile, &pem.Block{Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(&key)})
keyPEMFile.Close()
}
func loadKey(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := gob.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
下面这个程序可以读取这个证书
/* GenX509Cert
*/
package main
import (
"crypto/x509"
"fmt"
"os"
)
func main() {
certCerFile, err := os.Open("jan.newmarch.name.cer")
checkError(err)
derBytes := make([]byte, 1000) // bigger than the file
count, err := certCerFile.Read(derBytes)
checkError(err)
certCerFile.Close()
// trim the bytes to actual length in call to actual length in call
cert, err := x509.ParseCertificate(derBytes[0:count])
checkError(err)
fmt.Printf("Name %s\n", cert.Subject.CommonName)
fmt.Printf("Not before %s\n", cert.NotBefore.String())
fmt.Printf("Not after %s\n", cert.NotAfter.String())
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
TLS
如果自己实现所有的细节,加解密的方案在使用上是有限制的。当前互联网上最流行的加密消息传输方案是 TLS(Transport Layer Security 安全传输层)
,其前身为 SSL(Secure Sockets Layer安全套接字层)
。
在 TLS 中,客户端和服务器之间使用 X.509 证书进行身份验证。身份验证完成后,两者之间会生成一个密钥,所有的加密和解密过程都使用这个密钥。虽然客户端和服务端协商的过程相对较慢,但一旦完成就会使用一个较快的私钥机制。
服务器端程序
/* TLSEchoServer
*/
package main
import (
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"os"
"time"
)
func main() {
//加载X509秘钥对
cert, err := tls.LoadX509KeyPair("jan.newmarch.name.pem", "private.pem")
checkError(err)
config := tls.Config{Certificates: []tls.Certificate{cert}}
now := time.Now()
config.Time = func() time.Time{ return now }
config.Rand = rand.Reader
service := "0.0.0.0:1200"
listener, err := tls.Listen("tcp", service, &config)
checkError(err)
fmt.Println("Listening")
for{
conn, err := listener.Accept()
if err != nil {
fmt.Println(err.Error())
continue
}
fmt.Println("Accepted")
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for{
fmt.Println("Trying to read")
n, err := conn.Read(buf[0:])
if err != nil {
fmt.Println(err)
}
_, err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
与服务器端程序对应的客户端程序:
/* TLSEchoClient
*/
package main
import (
"fmt"
"os"
"crypto/tls"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
conn, err := tls.Dial("tcp", service, nil)
checkError(err)
for n := 0; n < 10; n++ {
fmt.Println("Writing...")
conn.Write([]byte("Hello "+ string(n+48)))
var buf [512]byte
err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
}
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
结论
安全本身是一个巨大的领域,在本章中,我们几乎没有触及,但已经覆盖了主要的概念。尚未强调的是,需要在设计阶段如何考虑安全构建:亡羊补牢几乎是没有意义的。
有疑问加站长微信联系(非本文作者)