文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 正文
    1. 3.1. X509证书认证逻辑
    2. 3.2. 相关文件扩展名的含义
    3. 3.3. 证书的制作
      1. 3.3.1. 第一步, 生成私钥+请求文件
      2. 3.3.2. 第二步, 生成CA证书
      3. 3.3.3. 第二步, 签发下级证书
      4. 3.3.4. 第三步, 如何校验证书的合法性
    4. 3.4. 开发中如何利用X509
      1. 3.4.1. 代码大概可以这么写
      2. 3.4.2. 安全认证的思路

文档更新说明

  • 最后更新 2020年07月22日
  • 首次更新 2020年07月22日

前言

入职安全公司一个月了, 很忙, 只剩下周日才有空(这句话可以细心品味一下😂), 周日还要去看房, 生活胖若两人. 趁今晚任务开发得差不多了, 抽空写一下博文, 算是对近一个月部分工作内容做个总结吧.

目前我负责MacOS桌面级服务进程的开发, 还有Mac GUI开发, 开发技术栈用的OC, C, 还有Golang, 接触的大部分都是之前做iOS没接触过的, 团队人数也挺多的, 基本上是对我以前短板的技能做一个的补充, 补充的技能包括: 参与中大型项目,多人协作,git管理,TCP/UDP通讯,多进程开发等, 对于之前做多了iOS应用层开发的我来说, 还是有很大进步的.

客户端用Golang? 这个我之前确实没想到, Golang一般是用在服务端的, 但是他在桌面服务进程上也能很好的发挥他的优势, 除了没有操作数据库之外, 其他都和服务端开发类似, 挺不错, 公司很多C++项目都慢慢转撑Golang了, 这算是以前学习Golang误打误撞, 变成今天工作利器了.

正文

说了这么多, 开始正文吧, 本文主要是总结一下近期用到的X509标准加密通讯, 这个标准确实是很不错, 很方便多端安全通讯, 实现起来也不复杂.

X509证书认证逻辑

可能有些人不知道什么是X509, 那应该知道HTTPS, TLS吧? 这两个协议用的就是X509证书认证原理, 还有平时做iOS开发用的那些证书杀的, 俗称数字签名, 用的都是X509这套东西, 我之前大多数是停留在理论知识的学习, 还有HTTPS这些的使用, 今天就来总结一下证书的生成, 自签名等逻辑, 以及在实际开发中如何发挥X509的威力!

相关文件扩展名的含义

先来说清楚X509体系里面一些不同格式的数据到底代表什么. 这部分摘自网络

我们已经知道有PEM和DER这两种编码格式,但文件扩展名并不一定就叫”PEM”或者”DER”,常见的扩展名除了PEM和DER还有以下这些,它们除了编码格式可能不同之外,内容也有差别,但大多数都能相互转换编码格式.

CRT - CRT应该是certificate的三个字母,其实还是证书的意思,常见于*NIX系统,有可能是PEM编码,也有可能是DER编码,大多数应该是PEM编码,相信你已经知道怎么辨别.

CER - 还是certificate,还是证书,常见于Windows系统,同样的,可能是PEM编码,也可能是DER编码,大多数应该是DER编码.

KEY - 通常用来存放一个公钥或者私钥,并非X.509证书,编码同样的,可能是PEM,也可能是DER.

JKS - 即Java Key Storage,这是Java的专利,跟OpenSSL关系不大,利用Java的一个叫”keytool”的工具,可以将PFX转为JKS,当然了,keytool也能直接生成JKS,不过在此就不多表了.

上面到两种编码格式, PEM是base64格式, DER则是二进制格式, 后续实例中, 我全部用的PEM格式, PEM格式比较方便在多端传输.

证书的制作

下面全部用OpenSSL示例, 具体对于到代码里怎么写,不同语言有不同的处理库, 只要用OpenSSL验证过, 基本没什么大问题

第一步, 生成私钥+请求文件

生成2048位私钥

openssl genrsa -out ca.key 2048

请求文件对于做iOS的估计很熟悉, 一般申请APP的开发证书, 苹果要你传一个证书请求文件上去, 就是这个了.

openssl req -new -sha256 -key ca.key -out ca.csr -subj "/C=CN/ST=GuangDong/L=ShenZhen/O=ROOT/OU=dc/CN=ROOT/emailAddress=test@xx.com"

subj字段的内容其实就是证书发行者的一些信息, 规定使用sha256算法, 也是目前啊最可靠的散列算法了.

其中CN字段对应的是公司名字, 或者HTTPS常见的网站域名, 不过这个都不重要, 后续我们如果是客户端对服务端通讯的话, 可以直接把CN省略, 或者验证的时候忽略掉即可.

第二步, 生成CA证书

openssl x509 -req -in ca.csr -out ca.crt -signkey ca.key -days 3650 -sha256 -extfile v3.ext

上面的-extfile是可选的, 如果指定扩展了信息, 则会生成V3版本证书. 需要先创建文件 v3.ext, 内容如下:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:TRUE
keyUsage = digitalSignature, keyCertSign #具有keyCertSign用途的证书才能签发下级证书

ext信息具体可以怎么填写, 可以参考这个文档, 说得很清楚https://www.openssl.org/docs/manmaster/man5/x509v3_config.html

到这里, 就可以得到一个具备签发其他证书的权威CA根证书了, 接着就可以用这个证书来签发一级证书, 二级证书等.

第二步, 签发下级证书

证书签发原理, 之前写的HTTPS原理的文章有说到, 现在是站在实战的角度来看.

记得一开始的生成CA证书的时候输入了私钥吗? 其实这种叫做自签名, 而且没有指定生成证书的CA参数, 这样生成的证书里头, Subject(发行者)和Issuer(签发者)是同一个人, 而且还用CA证书自己的私钥为自己签名了(其实签名不签名都可以, 因为发行者和签发者是同一个人, 则证书的公钥默认就是发行者的有效公钥, 无需后续验证)

这了假设生成服务端证书, 还是一样先生成私钥和请求文件

openssl genrsa -out server.key 2048
openssl req -new -sha256 -out server.csr -key server.key -subj "/C=CN/ST=GuangDong/L=ShenZhen/O=Server/OU=dc/CN=Server/emailAddress=test@datacloak.com"

利用CA证书, 为服务端证书做一个签名

openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -sha256 -extfile v3.ext

这里可以看出, 签发下级证书是需要上级证书的, 这里上级证书就是ca.crt, 这样服务端证书的Subject是服务端, 但是Issuer是CA证书的发行者. 到时候要认证服务端证书是否合法, 就要靠上级ca证书了

上面提到的签名, 数字签名的原理, 就是对一段数据, 这里是服务端证书数据部分, 做一次sha256散列运算, 得到256位运算结果, 再用上级CAkey进行加密后, 附在证书上.

到时候要认证服务端证书是否有效的人(比如浏览器), 就需要拿到签发者的公钥解密证书签名部分, 结合自己对服务端数据部分做sha256运算得到的值, 做一个比较, 如果结果一样, 那么服务端证书就是可信的. 这里应该说得很清楚了. HTTPS证书校验的核心原理也就是这块了.

注意: 如果ca证书不是根证书(也就是发行者和签发者不是同一个人), 那么就需要用同样的原理验证ca证书的合法性.

第三步, 如何校验证书的合法性

这一步是最重要的, 我们上面做了那么多证书, 目的就是为了验证合法性, 这样才能确定和自己通讯的一端是否是合法的.

验证二级证书是否合法, 只需要一个根证书即可验证

openssl verify -CAfile ca.crt server.crt

如果是验证多级证书是否合法, 则需要把证书的全部上级证书链都放进来一起验证

# 如果有多个证书链, 则需要把每个上级证书的文件名改成 hash值.0(比如7a191709.0), 并且指定整个目录
openssl x509 -hash -in ca.crt #输出第一行8个字符就是哈希值
openssl verify -CApath ./ server.crt
# 或者把证书链都合并到证书内容里, 证书看起来就有多个begin-end标志

上面说的把多个证书合并在一起, 合并的顺序看不同编程语言的要求. 如果是控制台用openssl的话, 上级证书放顶部, 如果是某些golang的库, 则上级证书放底部, 具体看文档说明.

开发中如何利用X509

代码大概可以这么写

上面说的是证书怎么制作, 具体到怎么用具体编程语言来动态生成证书, 这个还得具体看文档才知道. 我只做过golang版本的, 下面直接贴代码吧, 利用已经有的CA证书, 签发新的下级证书:

package internal
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
rd "math/rand"
"strings"
"time"
)
type CertInformation struct {
Country []string
Organization []string
OrganizationalUnit []string
EmailAddress []string
Province []string
Locality []string
CommonName string
IsCA bool
Names []pkix.AttributeTypeAndValue
}
func newCertificate(info CertInformation) *x509.Certificate {
return &x509.Certificate{
SerialNumber: big.NewInt(rd.Int63()),
Subject: pkix.Name{
Country: info.Country,
Organization: info.Organization,
OrganizationalUnit: info.OrganizationalUnit,
Province: info.Province,
CommonName: info.CommonName,
Locality: info.Locality,
ExtraNames: info.Names,
},
NotBefore: time.Now(), //证书的开始时间
NotAfter: time.Now().AddDate(20, 0, 0), //证书的结束时间
BasicConstraintsValid: true, //基本的有效性约束
IsCA: info.IsCA, //是否是根证书
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, //证书用途
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
EmailAddresses: info.EmailAddress,
}
}
// CreateCRTByPublicKey .
func CreateCRTByPublicKey(RootCa *x509.Certificate, RootKey *rsa.PrivateKey, publicKey *rsa.PublicKey, info CertInformation) (string, error) {
Crt := newCertificate(info)
var err error
var buf []byte
//使用根证书签名
buf, err = x509.CreateCertificate(rand.Reader, Crt, RootCa, publicKey, RootKey)
if err != nil {
return "", err
}
// 把证书转成pem格式, 并返回
var b *pem.Block = &pem.Block{Bytes: buf, Type: "CERTIFICATE"}
cert := pem.EncodeToMemory(b)
if cert == nil {
return "", errors.New("An error was encountered while converting pem format")
}
return string(cert), nil
}
// ParseCrt pem格式证书转换成Certificate实例
func ParseCrt(buf []byte) (*x509.Certificate, error) {
p := &pem.Block{}
p, _ = pem.Decode(buf)
return x509.ParseCertificate(p.Bytes)
}
// ParsePublicKey 转换pem格式的公钥为PublicKey实例
func ParsePublicKey(pemBuf []byte) (*rsa.PublicKey, error) {
// 获取dproxy传入的公钥
block, _ := pem.Decode([]byte(pemBuf))
if block == nil {
return nil, fmt.Errorf("pemBuf error")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
if !strings.Contains(err.Error(), "ParsePKCS1PublicKey") {
return nil, err
}
return x509.ParsePKCS1PublicKey(block.Bytes)
}
_, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("It is no a rsa public key")
}
return pub.(*rsa.PublicKey), nil
}
// ParseKey 转换pem格式私钥
func ParseKey(buf []byte) (*rsa.PrivateKey, error) {
p, _ := pem.Decode(buf)
return x509.ParsePKCS1PrivateKey(p.Bytes)
}
func X509CertToPEMString(cert *x509.Certificate) string {
var b *pem.Block = &pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}
parentPemCert := pem.EncodeToMemory(b)
return string(parentPemCert)
}
// Sign 签名数据, 返回签名结果
func Sign(data []byte, pri *rsa.PrivateKey) ([]byte, error) {
// 计算hash值, 然后用私钥签名
h := sha256.New()
h.Write(data)
digest := h.Sum(nil)
var opts rsa.PSSOptions
opts.SaltLength = rsa.PSSSaltLengthEqualsHash
opts.Hash = crypto.SHA256
signData, err := pri.Sign(rand.Reader, digest, &opts)
if err != nil {
Logger.Error(fmt.Sprintf("sign err:%s", err))
return nil, err
}
return signData, err
}

使用方法, 就是直接调用CreateCRTByPublicKey函数, 传入要签发的公钥和CA证书+私钥. 另外的一些函数都是一些格式换行函数.

其中PEM格式是base64, 也就是字符串, 传参的时候直接把每一个字符都看成一个byte而已, 其实就是[]byte(“xxxxx”).

上面一大坨代码, 看不懂也无所谓, 查查文档就懂了. 重点是证书签发的流程.

安全认证的思路

认证思路, 总结起来大概就是几点

第一种, 数字签名

  1. 对数据做数字摘要, 用私钥加密, 也就是数字签名.
  2. 接收方收到数据, 结合发送放的证书信息, 提取公钥解密, 解密签名信息, 再和数据部分对比较, 确认数据完整性.

这种可以用在文件下载上, 对请求方做一个签名验证. 也可以用在对文件有效性判断上, 比如苹果系统用的最多的数字签名验证.

第二种, 可信任校验

这个就是HTTPS那种, 是实现TLS层的基石, 本质就是校验对方公钥是否可信, 从而明确是否信任对方, 并用对方的公钥解密数据, 可以做单向校验或者双向校验.

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 正文
    1. 3.1. X509证书认证逻辑
    2. 3.2. 相关文件扩展名的含义
    3. 3.3. 证书的制作
      1. 3.3.1. 第一步, 生成私钥+请求文件
      2. 3.3.2. 第二步, 生成CA证书
      3. 3.3.3. 第二步, 签发下级证书
      4. 3.3.4. 第三步, 如何校验证书的合法性
    4. 3.4. 开发中如何利用X509
      1. 3.4.1. 代码大概可以这么写
      2. 3.4.2. 安全认证的思路