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

# 生成私钥证书

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

```go
// 生成 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,
}
```

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

`KeyUsage` 为 `KeyUsageCertSign` 和 `KeyUsageCRLSign`，支持签发和吊销中级证书。这里不需要 `ExtKeyUsage`。

## 中级证书

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

```go
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,
}
```

## 终端证书

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

```go
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`，也不需要配置`MaxPathLen`和`MaxPathLenZero`了。

`KeyUsage` 为 `KeyUsageDigitalSignature` 和 `KeyUsageKeyEncipherment`，可用于身份验证和数据加密传输。`ExtKeyUsage` 为 `ExtKeyUsageServerAuth` 可以进行服务端身份验证。

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


# 证书签名

证书签名可以使用标准库 `crypto/x509` 的 `CreateCertificate` 方法来签名。该方法需要以下5个参数：

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


具体签证方法如下：

根证书：

```go
rootDer, err := x509.CreateCertificate(rand.Reader, rootCsr, rootCsr, rootKey.Public(), rootKey)

rootCert, err := x509.ParseCertificate(rootDer)
```

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

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

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

interCert, err := x509.ParseCertificate(interDer)
```

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

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

cert, err := x509.ParseCertificate(der)
```

# 保存 PEM 文件

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

签证证书：

```go
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` 方法可以帮助我们从文件读取证书，然后稍微做一下转换就行了。

```go
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
}
```

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


# 参考

1. [OpenSSL Certificate Authority](https://jamielinux.com/docs/openssl-certificate-authority/index.html)
2. [Hyperledger Fabric-ca配置文件解读](https://www.its404.com/article/weixin_42803027/119755176)
3. [OpenSSL CA keyUsage 扩展](https://qa.1r1g.cn/superuser/ask/51702871/)
4. [RFC 5280 section 4.2.1.12 Extended Key Usage](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12)


