添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

使用Go语言生成自签CA证书

使用Go语言生成自签CA证书,可以保存为PEM文件,也可以从PEM文件读取证书,还可以动态生成终端域名证书。

生成私钥证书

不管是根证书,中级证书还是终端域名证书,都需要先生成一个私钥,然后通过私钥来获取公钥再进行证书签名,OpenSSL 可以通过 ecparam 子命令生成 ECC 私钥证书,Go 的标准库 crypto/ecdsa 也提供了 ECC 生成私钥的方法,我们稍微进行一下封装:

// 生成 ECC 私钥
func GeneratePrivateKey() (key *ecdsa.PrivateKey) {
    key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    return

这里的 elliptic.P256() 就是 prime256v1,这样我们就有了一个通用的生成 ECC 私钥的方法了。

构建证书签名请求 CSR

Go 提供了标准库 crypto/x509 给我们提供了 x509 签证的能力,我们可以先通过 x509.Certificate 构建证书签名请求 CSR 然后再进行签证。

x509.Certificate 参数解析

x509.Certificate 有以下参数可配置:

  • Version: 证书的版本,数字1,2,3
  • SerialNumber:证书序列号,标识证书的唯一整数,重复的编号无法安装到系统里。
  • SignatureAlgorithm: 签证书的算法标识,缺省。
  • PublicKeyAlgorithm: 生成公钥的算法,缺省。
  • Subject:证书持有者的信息。

  • Country: 国家,CN。
  • Province: 省。
  • Locality: 市。
  • Organization: 证书持有者组织名称。
  • OrganizationalUnit: 证书持有者组织唯一标识。
  • CommonName: 证书持有者通用名,需保持唯一,否则验证会失败。
  • NotBefore: 证书有效期开始时间。

  • NotAfter: 证书过期时间。

  • EmailAddresses: 需要颁发证书的邮箱地址。

  • DNSNames: 需要颁发证书的 DNS,也就是域名。
  • IPAddresses: 需要颁发证书的 IP 地址。
  • URIs: 需要颁发证书的 URI。

  • BasicConstraintsValid: 为true表示IsCA/MaxPathLen/MaxPathLenZero有效,为false忽略这几个配置。

  • IsCA: 是否为CA证书,CA证书可以为下级证书签证,为false代表是终端证书,不能继续签证,根证书和中级证书都应该为true
  • MaxPathLen: 表示证书链中可在此证书之后的非自颁发中级证书的最大层级,我们只需要1个中级证书就可以了,根证书设置为1,中级证书设置为0,那么中级证书就不能继续签署中级证书了。-1 表示未设置,且MaxPathLenZero == false && MaxPathLen == 0视为-1
  • MaxPathLenZero: MaxPathLen == 0

  • KeyUsage: 定义了证书包含的密钥的用途。

  • KeyUsageDigitalSignature: 用于数字签名,常用于具有完整性的实体身份验证和数据源身份验证。可以用于CA证书或终端证书。
  • KeyUsageContentCommitment: 公钥可用于提供不可否认服务,这可以防止签名实体错误地拒绝某些操作。当发生冲突时,应该有一个可靠的第三方来对签名的数据进行辨伪。
  • KeyUsageKeyEncipherment: 用于加密对称密钥,目标解密密钥,随后使用它来加密和解密实体之间的数据。
  • KeyUsageDataEncipherment: 用于加密和解密实际应用程序数据。
  • KeyUsageKeyAgreement: 使用密钥协商协议与目标建立对称密钥,然后可以使用对称密钥来加密和解密实体之间发送的数据。
  • KeyUsageCertSign: 用于校验公钥证书的签名,只能用于 CA 证书。
  • KeyUsageCRLSign: 用于验证证书吊销列表的签名,只能用于 CA 证书。
  • KeyUsageEncipherOnly: 公钥仅用于在执行密钥协商时加密数据。
  • KeyUsageDecipherOnly: 公钥仅用于在执行密钥协商时解密数据。
  • ExtKeyUsage: 该扩展表示被认证的公钥的用途,可以替换或作为KeyUsage扩展的补充

  • ExtKeyUsageAny: 未知。
  • ExtKeyUsageServerAuth: 建立 TLS 连接时进行服务器身份验证。
  • ExtKeyUsageClientAuth: 建立 TLS 连接时进行客户端验证。
  • ExtKeyUsageCodeSigning: 对可下载执行的代码签名。
  • ExtKeyUsageEmailProtection: 安全电子邮件签名,允许发送和接收加密的电子邮件。
  • ExtKeyUsageIPSECEndSystem: IP 安全终端系统,已弃用。
  • ExtKeyUsageIPSECTunnel: IP 安全隧道,已弃用。
  • ExtKeyUsageIPSECUser: IP 安全用户,已弃用。
  • ExtKeyUsageTimeStamping: 可信时间戳。
  • ExtKeyUsageOCSPSigning: OCSP 签名。
  • ExtKeyUsageMicrosoftServerGatedCrypto: 未知。
  • ExtKeyUsageNetscapeServerGatedCrypto: 未知。
  • ExtKeyUsageMicrosoftCommercialCodeSigning: 未知。
  • ExtKeyUsageMicrosoftKernelCodeSigning: 未知。
  • 我们使用这些字段就够了,其他字段缺省即可。可以使用这些参数分别签证生成根证书、中级证书和终端域名证书的签证 CSR。

    var rootCsr = &x509.Certificate{
        Version:      3,
        SerialNumber: big.NewInt(time.Now().Unix()),
        Subject: pkix.Name{
            Country:            []string{"CN"},
            Province:           []string{"Shanghai"},
            Locality:           []string{"Shanghai"},
            Organization:       []string{"JediLtd"},
            OrganizationalUnit: []string{"JediProxy"},
            CommonName:         "Jedi Root CA",
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0),
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            1,
        MaxPathLenZero:        false,
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
    

    根证书的配置如上,CommonNameJedi Root CA,用来区分这是一个根证书,过期时间设置为十年,根证书十年足够了,MaxPathLen 为1,只能签发一级中级CA证书。

    KeyUsageKeyUsageCertSignKeyUsageCRLSign,支持签发和吊销中级证书。这里不需要 ExtKeyUsage

    中级证书的配置基本与根证书基本一致,只是需要改一下 MaxPathLen为0,过期时间短一些:

    var interCsr = &x509.Certificate{
        Version:      3,
        SerialNumber: big.NewInt(time.Now().Unix()),
        Subject: pkix.Name{
            Country:            []string{"CN"},
            Province:           []string{"Shanghai"},
            Locality:           []string{"Shanghai"},
            Organization:       []string{"JediLtd"},
            OrganizationalUnit: []string{"JediProxy"},
            CommonName:         "Jedi Inter CA",
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(1, 0, 0),
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            0,
        MaxPathLenZero:        true,
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
    

    终端域名证书的配置需要做一些变更,配置如下:

    var csr = &x509.Certificate{
        Version:      3,
        SerialNumber: big.NewInt(time.Now().Unix()),
        Subject: pkix.Name{
            Country:            []string{"CN"},
            Province:           []string{"Shanghai"},
            Locality:           []string{"Shanghai"},
            Organization:       []string{"JediLtd"},
            OrganizationalUnit: []string{"JediProxy"},
            CommonName:         "foreverz.cn",
        DNSNames:              []string{"foreverz.cn"},
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(1, 0, 0),
        BasicConstraintsValid: true,
        IsCA:                  false,
        KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    

    因为是终端域名证书,所以DNSNames需要设置为对应的域名,可以使用通配符*支持所有的三级域名,例如这里可以写成*.foreverz.cn,也可以使用一个整数签发多个域名。

    因为不是CA证书了,所以IsCA设为false,也不需要配置MaxPathLenMaxPathLenZero了。

    KeyUsageKeyUsageDigitalSignatureKeyUsageKeyEncipherment,可用于身份验证和数据加密传输。ExtKeyUsageExtKeyUsageServerAuth 可以进行服务端身份验证。

    这样我们就配置好了三级证书的签名请求了,这里都是分开写的,大部分代码都是一样的,可以写成一个方法,这里就不扩展了。

    证书签名可以使用标准库 crypto/x509CreateCertificate 方法来签名。该方法需要以下5个参数:

  • rand: 随机数,使用 rand.Reader 即可。
  • template: 证书签名请求,即上面的 CSR。
  • parent: 父级证书,根证书是自签的,直接用自己的 csr,中级证书用根证书来签名,终端证书使用中级证书签名。
  • pub: 第一步生成的私钥对应的公钥证书,可以使用 key.Public() 获取。
  • priv: 父级证书私钥。
  • 具体签证方法如下:

    rootDer, err := x509.CreateCertificate(rand.Reader, rootCsr, rootCsr, rootKey.Public(), rootKey)
    rootCert, err := x509.ParseCertificate(rootDer)
    

    CreateCertificate 返回的是一个 []byte,是二进制DER编码的证书,可以使用 x509.ParseCertificate 转为 *x509.Certificate 格式。

    生成了根证书就可以使用根证书签证中级证书了:

    interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCert, interKey.Public(), rootKey)
    interCert, err := x509.ParseCertificate(interDer)
    

    终端证书的生成与中级证书基本一致:

    der, err := x509.CreateCertificate(rand.Reader, csr, interCert, key.Public(), interKey)
    cert, err := x509.ParseCertificate(der)
    

    保存 PEM 文件

    上面生成的证书是 *x509.Certificate 格式的,我们需要转为 PEM 并存为 .pem 文件到本地才能安装到电脑上,可以通过以下方法进行转换:

    签证证书:

    certBlock := &pem.Block{
        Type:  "CERTIFICATE",
        Bytes: cert.Raw,
    pemData := pem.EncodeToMemory(certBlock)
    if err = ioutil.WriteFile("xx.cert.pem", pemData, 0644); err != nil {
            panic(err)
    

    私钥证书:

    keyDer, err := x509.MarshalECPrivateKey(key)
    keyBlock := &pem.Block{
        Type:  "EC PRIVATE KEY",
        Bytes: keyDer,
    keyData :=  := pem.EncodeToMemory(certBlock)
    if err = ioutil.WriteFile("xx.key.pem", keyData, 0644); err != nil {
            panic(err)
    

    从 PEM 文件读取证书

    上面我们把证书文件转为了 PEM 文件,那么有时候就会从 PEM 文件读取证书,我们没必要手写文件读取方法,crypto/tls 提供了 LoadX509KeyPair 方法可以帮助我们从文件读取证书,然后稍微做一下转换就行了。

    func LoadPair(certFile, keyFile string) (cert *x509.Certificate, err error) {
        if len(certFile) == 0 && len(keyFile) == 0 {
            return nil, errors.New("cert or key has not provided")
        // load cert and key by tls.LoadX509KeyPair
        tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
        if err != nil {
            return
        cert, err = x509.ParseCertificate(tlsCert.Certificate[0])
        return
    

    如上,我们完成了证书的生成、存储和读取等能力,证书相关的处理就结束了。