2022年9月

前言

本文为以下两篇文章的续文:

使用OpenSSL搭建简单一级CA

使用OpenSSL搭建简单多级CA与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)

23231501.png

签发最终证书

同上文所述:

# "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)

23232401.png

生成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)

23235201.png

搭建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)
}

23230001.png

参考

https://pkg.go.dev/crypto/x509

https://pkg.go.dev/golang.org/x/crypto/ocsp

https://github.com/grimm-co/GOCSP-responder