使用Golang X509签发证书及构建CA架构
前言
本文为以下两篇文章的续文:
本文有一些概念涉及前文,故建议阅读后再实践本文章。
生成与解析csr
生成密钥
# "crypto/rand","crypto/rsa"
priKey, err := rsa.GenerateKey(rand.Reader, 2048)
// error_check
pubKey := priKey.Public()
保存密钥
密钥通常使用PEM
格式保存,以开头,以
结尾,中间是使用Base64
编码的内容,为方便下文的操作,封装成一个工具函数:
# "encoding/pem"
func PEMEncoding(key []byte, blockType int) []byte {
keyType := "PUBLIC KEY"
if blockType == 1 {
keyType = "RSA PRIVATE KEY"
} else if blockType == 2 {
keyType = "CERTIFICATE REQUEST"
} else if blockType == 3 {
keyType = "CERTIFICATE"
} else if blockType == 4 {
keyType = "X509 CRL"
}
block := &pem.Block{
Type: keyType,
Bytes: key,
}
return pem.EncodeToMemory(block)
}
写入文件:
# "crypto/x509"
// 使用 PKCS1 编码密钥
priKeySource := x509.MarshalPKCS1PrivateKey(priKey)
pubKeySource, err := x509.MarshalPKIXPublicKey(pubKey)
// error_check
pubKeySource := x509.MarshalPKCS1PrivateKey(priKey)
_ = os.WriteFile("ca_pri.key", PEMEncoding(priKeySource, 1), 0755)
_ = os.WriteFile("ca_pub.key", PEMEncoding(pubKeySource, 0), 0755)
生成请求
生成证书请求需要使用x509.CertificateRequest
结构体:
# "crypto/x509"
x509.CertificateRequest{
// 经过 DER 编码的ASN.1信息(包含CSR、签名算法和签名)
// 读取时自动填充
Raw: nil,
// 经过 DER 编码的证书请求信息
RawTBSCertificateRequest: nil,
// 经过 DER 编码的公钥信息
RawSubjectPublicKeyInfo: nil,
// 经过 DER 编码的主题信息
RawSubject: nil,
// CSR版本
Version: 0,
// 签名
Signature: nil,
// 签名算法
SignatureAlgorithm: 0,
// 公钥算法
PublicKeyAlgorithm: 0,
// 公钥
PublicKey: nil,
// 主题信息
Subject: pkix.Name{},
// 已弃用,可被解析的 CSR 信息
Attributes: nil,
// CSR 的扩展信息
Extensions: nil,
// CSR 的原始扩展信息,当这项不为空时将覆盖其他指定的扩展
// 解析时不会填充
ExtraExtensions: nil,
// 主题备用名称
DNSNames: nil,
EmailAddresses: nil,
IPAddresses: nil,
URIs: nil,
}
生成请求:
# "crypto/x509","crypto/rand"
caRequest := x509.CertificateRequest{
Subject: pkix.Name{
Country: []string{"CN"},
Organization: []string{"blog.yeziruo.com"},
CommonName: "Yeziruo Root CA",
},
}
// 随机数流,CertificateRequest,私钥
csr, err := x509.CreateCertificateRequest(rand.Reader, &caRequest, priKey)
// error_check
_ = os.WriteFile("ca_req.csr", PemEncoding(csr, 2), 0755)
解析请求
使用x509.ParseCertificateRequest
函数来解析CSR
文件,返回CertificateRequest
对象:
# "crypto/x509"
caRequest, err = x509.ParseCertificateRequest(csr)
// error_check
签发根证书
证书的签发需要使用x509.Certificate
结构体,但该结构体参数过多,故只解释本文所使用的:
# "crypto/x509"
x509.Certificate{
// 颁发者主题
Issuer: pkix.Name{},
// 被颁发者主题
Subject: pkix.Name{},
// 不早于
NotBefore: time.Time{},
// 不晚于
NotAfter: time.Time{},
// 基础密钥用法
KeyUsage: 0,
// 扩展密钥用法
ExtKeyUsage: nil,
// 基本约束
BasicConstraintsValid: false,
// 是证书颁发机构
IsCA: false,
// 证书最大路径,该级 CA 能签发子 CA 的证书链路径深度
MaxPathLen: 0,
// 不允许该 CA 签发子 CA
MaxPathLenZero: false,
// OCSP 响应服务器
OCSPServer: nil,
// 备用 DNS 名称
ExcludedDNSDomains: nil,
// CRL 分发点
CRLDistributionPoints: nil,
}
序列号生成:
# rand2 "math/rand"
// sn := big.NewInt(rand2.Int63())
sn := big.NewInt(rand2.Uint64())
// 序列号不能为默认,也不能为有序序号,必须是一个很大的随机数
// ”序列号熵“问题
生成根证书:
# "crypto/x509"
ca := x509.Certificate{
// 从 CSR 中复制主题信息
Subject: caRequest.Subject,
// 序列号
SerialNumber: sn,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(5, 0, 0),
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
}
// 随机数流,被颁发者,颁发者,被颁发者公钥,颁发者私钥
// 可以通过这个函数简单的新建子 CA
crt, err := x509.CreateCertificate(rand.Reader, &ca, &ca, pubKey, priKey)
// error_check
_ = os.WriteFile("ca.crt", PEMEncoding(crt, 3), 0755)
签发最终证书
同上文所述:
# "crypto/x509","crypto/rand","crypto/rsa",rand2 "math/rand"
cliPriKey, err := rsa.GenerateKey(rand.Reader, 2048)
// error_check
_ = os.WriteFile("cli_pri.key", PEMEncoding(x509.MarshalPKCS1PrivateKey(cliPriKey), 1), 0755)
sn = big.NewInt(rand2.Int63())
cli := x509.Certificate{
Subject: pkix.Name{CommonName: "local1.yeziruo.com"},
DNSNames: []string{"local1.yeziruo.com", "local2.yeziruo.com"},
SerialNumber: sn,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
BasicConstraintsValid: true,
IsCA: false,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
CRLDistributionPoints: []string{"http://192.168.122.250/ca.crl"},
OCSPServer: []string{"http://192.168.122.250/"},
}
cliCrt, err := x509.CreateCertificate(rand.Reader, &cli, &ca, cliPriKey.Public(), priKey)
// error_check
_ = os.WriteFile("cli.crt", PEMEncoding(cliCrt, 3), 0755)
生成CRL
列表
// 重新读取证书,让其自动填充一些信息
cca, err := x509.ParseCertificate(crt)
// error_check
revokedObj := pkix.RevokedCertificate{
// 被吊销证书序列号
SerialNumber: cli.SerialNumber,
// 吊销时间
RevocationTime: time.Now(),
}
revokedList := x509.RevocationList{
// 签发者主题信息
Issuer: ca.Subject,
// 被吊销的证书列表
RevokedCertificates: []pkix.RevokedCertificate{revokedObj},
// 一个自增的 CRL 序列号
Number: big.NewInt(1),
// 生成时间
ThisUpdate: time.Time{},
// 下一次更新时间
NextUpdate: time.Time{},
}
list, err := x509.CreateRevocationList(rand.Reader, &revokedList, cca, priKey)
// error_check
_ = os.WriteFile("ca.crl", PEMEncoding(list, 4), 0755)
搭建OCSP
响应服务器
OCSP
协议使用HTTP
承载,同时解析请求需要用到扩展加密库:
package main
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"golang.org/x/crypto/ocsp"
"net/http"
"os"
"time"
)
func main() {
http.HandleFunc("/", handler)
fmt.Println("http://localhost:8081/")
err := http.ListenAndServe("localhost:8081", nil)
if err != nil {
panic(err)
}
}
func loadPriKey(fn string) (*rsa.PrivateKey, error) {
key, err := os.ReadFile(fn)
if err != nil {
return nil, err
}
block, _ := pem.Decode(key)
priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return priKey, nil
}
func loadCertificate(fn string) (*x509.Certificate, error) {
key, err := os.ReadFile(fn)
if err != nil {
return nil, err
}
block, _ := pem.Decode(key)
crt, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return crt, nil
}
// no nonce ext support
// https://github.com/grimm-co/GOCSP-responder
func handler(w http.ResponseWriter, r *http.Request) {
ca, err := loadCertificate("ca.crt")
cli, err := loadCertificate("cli.crt")
priKry, err := loadPriKey("ca_pri.key")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.Header.Get("Content-Type") != "application/ocsp-request" && r.Method != "POST" {
w.WriteHeader(http.StatusBadRequest)
return
}
blob := new(bytes.Buffer)
_, err = blob.ReadFrom(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
//request, err := ocsp.ParseRequest(blob.Bytes())
//if err != nil {
// return
//}
status := ocsp.Response{
Status: ocsp.Revoked,
SerialNumber: cli.SerialNumber,
ProducedAt: cli.NotBefore,
ThisUpdate: time.Now(),
NextUpdate: time.Now().AddDate(0, 0, 15),
RevokedAt: time.Now().AddDate(0, 0, -1),
RevocationReason: 0,
// Certificate: ca,
}
response, err := ocsp.CreateResponse(ca, cli, status, priKry)
if err != nil {
return
}
w.Write(response)
}
参考
https://pkg.go.dev/crypto/x509