# 使用ECC算法自签Ca证书

# 概览

前一篇博客 [CA & OpenSSL自签名证书](https://foreverz.cn/cert) 讲了如何使用 OpenSSL 生成自签名证书，用的是 RSA 算法，RSA 算法是比较常见的算法，应用较早，较为普及，兼容性较好，但是 RSA 算法密钥长度较长，性能消耗较高。后来于 1985 年提出 [椭圆曲线密码学](https://zh.m.wikipedia.org/zh-hans/%E6%A4%AD%E5%9C%86%E6%9B%B2%E7%BA%BF%E5%AF%86%E7%A0%81%E5%AD%A6)，简称 ECC，并于 2004 年开始被广泛应用。 ECC 被认为是 RSA 的继任者，新一代的非对称加密算法，尤其在区块链中广泛使用。

关于 RSA 和 ECC 算法的原理这里不做介绍，我们只需要知道这是两种非对称加密算法就可以了，而 ECC 加密速度快，效率更高，对服务器资源消耗低，而且重要的是更安全，抗攻击型更强。所以这里介绍一下如何使用 ECC 替换 RSA 生成 HTTPS 证书。

想详细了解 ECC 椭圆算法的可以看文字后面的参考<sup>[1][2]</sup>。

# OpenSSL ecparam 命令

OpenSSL 使用 `ecparam` 命令来进行 ECC 加密，我们看一下支持的参数：

* `-inform PEM|DER`: 输入文件格式，DER或者PEM格式。DER格式采用ASN1的DER标准格式。一般用的多的都是PEM格式，就是base64编码格式。
* `-outform DER|PEM`: 输出文件格式，DER或者PEM格式。
* `-in filename`: 输入的椭圆曲线密钥文件，默认为标准输入。
* `-out filename`: 椭圆曲线密钥输出文件，默认为标准输出。
* `-noout`: 不打印参数编码的版本信息。
* `-text`：打印椭圆曲线密钥参数信息值。
* `-C`：以C语言风格打印信息。
* `-check`：检查椭圆曲线密钥参数。
* `-name arg`：采用短名字。
* `-list_curve`：打印所有可用的短名字。
* `-param_enc`：指定参数编码方法，可以是 `named_curve` 和 `explicit`，默认为`named_curve`。
* `-conv_form arg`：指定信息存放方式，可以是 `compressed`、`uncompressed` 或者`hybrid`，默认为`compressed`。
* `-no_seed`：如果 `-param_enc` 指定编码方式为 `explicit`，不采用随机数种子。
* `-rand file(s)`：随机数产生种子。
* `-genkey`：生成椭圆曲线密钥参数。
* `-engine id`：指定硬件引擎。

注意：

PEM格式的EC参数用的头部和底部为：

```shell
-----BEGIN EC PARAMETERS-----
...
-----END EC PARAMETERS-----
```

## 示例

* 查看内置的椭圆曲线：

```shell
openssl ecparam -list_curves
```

我们可以用内置的椭圆曲线创创建 EC 参数。

* 用曲线 `prime192v1` 产生 EC 参数：

```shell
openssl ecparam -name prime192v1 -out ec_param.pem
```

* 用明确的参数来产生 EC 参数：

```shell
openssl ecparam -name prime192v1 -param_enc explicit -out ec_param.pem
```

* 验证给出的 EC 参数：

```shell
openssl ecparam -in ec_param.pem -check 
```

* 创建 EC 参数和私钥文件：

```shell
openssl ecparam -genkey -name prime192v1 -out ec_key.pem 
```

* 改变编码为 `compressed`：

```shell
openssl ecparam -in ec_param.pem -conv_form compressed -out ec_out.pem 
```

* 查看 EC 私钥：

```shell
openssl ecparam -in ec_param.pem -noout -text
```

# 使用 ECC 自建CA

使用 ECC 和使用签名使用 RAS 的基本一致，不过是把生成私钥的命令从 `genrsa` 换成 `ecparam` 了而已，完整命令如下：

```shell
#!/bin/bash
set -ex

mkdir root

# 创建 Root Key
openssl ecparam -out root/ca.key.pem -name prime256v1 -genkey

curl -o ./root/openssl.cnf https://gist.githubusercontent.com/foreverzmy/0b07d3a731d70519e35ced070118e3fe/raw/71210c2643b8997ea6e2cf333e26aa33e712a7e2/simple-root-ca.cnf

# 创建 Root Cert
openssl req -config root/openssl.cnf \
      -key root/ca.key.pem \
      -passin pass:123456 \
      -new \
      -x509 \
      -days 7300 \
      -sha256 \
      -extensions v3_ca \
      -out root/ca.cert.pem \
      -subj /C=CN/ST=Shanghai/L=Shanghai/O=JediLtd/OU=JediProxy/CN=JediRootCA/emailAddress=921255465@qq.com

chmod 400 root/ca.cert.pem

# 验证证书
# openssl x509 -noout -text -in root/ca.cert.pem

# 创建 Intermediate Pair

# 创建 intermediate 文件夹存放 Intermediate Pair
mkdir intermediate

# 创建 Intermediate Key
openssl ecparam -out intermediate/intermediate.key.pem -name prime256v1 -genkey

chmod 400 intermediate/intermediate.key.pem

curl -o ./intermediate/openssl.cnf https://gist.githubusercontent.com/foreverzmy/936b1bb55adf52c5d1accc9b2216a0e3/raw/28b9f97df967d93febf09219863190aea3b36895/simple-intermediate-ca-cnf

# 创建 Intermediate Cert
openssl req -config intermediate/openssl.cnf -new -sha256 \
      -key intermediate/intermediate.key.pem \
      -passin pass:123456 \
      -out intermediate/intermediate.csr.pem \
      -subj /C=CN/ST=Shanghai/L=Shanghai/O=JediLtd/OU=JediProxy/CN=JediIntermediateCA/emailAddress=921255465@qq.com

chmod 400 intermediate/intermediate.key.pem

touch root/index.txt
echo 1000 > root/serial
# 使用 v3_intermediate_ca 扩展签名，密码为 123456
# Intermediate Pair 的有效时间一定要为 Root Pair 的子集
openssl ca -config root/openssl.cnf \
      -extensions v3_intermediate_ca \
      -days 3650 \
      -notext \
      -md sha256 \
      -passin pass:123456 \
      -in intermediate/intermediate.csr.pem \
      -out intermediate/intermediate.cert.pem

chmod 444 intermediate/intermediate.cert.pem

# 验证 Intermediate Pair
# openssl x509 -noout -text -in intermediate/intermediate.cert.pem
# openssl verify -CAfile root/ca.cert.pem intermediate/intermediate.cert.pem

# 浏览器在验证中级证书的时候，同时也会去验证它的上一级证书是否靠谱
# 创建证书链，将 Root Cert 和 Intermediate Cert 合并到一起，可以让浏览器一并验证
cat intermediate/intermediate.cert.pem root/ca.cert.pem > intermediate/ca-chain.cert.pem
chmod 444 intermediate/ca-chain.cert.pem

# 创建服务器/客户端证书

# 创建 foreverz.cn 存放域名相关证书文件
mkdir foreverz.cn

# 生成域名私钥
openssl ecparam -out foreverz.cn/foreverz.cn.key.pem -name prime256v1 -genkey

# 创建网站证书请求
openssl req -config intermediate/openssl.cnf \
      -key foreverz.cn/foreverz.cn.key.pem \
      -new -sha256 \
      -out foreverz.cn/foreverz.cn.req \
      -subj /C=CN/ST=Shanghai/L=Shanghai/O=JediLtd/OU=JediProxy/CN=foreverz.cn/emailAddress=921255465@qq.com

# 签发网站证书
openssl x509 -req -in foreverz.cn/foreverz.cn.req \
      -days 375 \
      -sha256 \
      -CA intermediate/intermediate.cert.pem \
      -CAkey intermediate/intermediate.key.pem \
      -CAcreateserial \
      -out foreverz.cn/foreverz.cn.cert.pem

# 验证证书
# openssl x509 -noout -text -in foreverz.cn/foreverz.cn.cert.pem
openssl verify -CAfile intermediate/ca-chain.cert.pem foreverz.cn/foreverz.cn.cert.pem
```

这样就生成了一系列证书，下面是我们需要的文件：

* `ca.key.pem`: 根CA私钥文件，签发中级CA证书时需要。
* `ca.cert.pem`: 根CA证书，可以安装到电脑上，用来认证中级CA。
* `intermediate.key.pem`：中级CA私钥，签到域名证书是需要。
* `intermediate.key.pem`：中级CA证书，可以安装到电脑上，用来认证域名证书。
* `ca-chain.cert.pem`: CA证书链，包含根证书和中级证书，可以一次性安装两个证书。
* `foreverz.cn.key.pem`: 网站私钥，启用 HTTPS 服务时需要
* `foreverz.cn.cert.pem`: 网站公钥，启用 HTTPS 服务时需要

# 测试

下面我们启动一个 HTTPS 服务测试一下证书。由于我比较喜欢 go 语言，所以使用 go 语言的 [fasthttp](https://github.com/valyala/fasthttp) 框架来测试。

在 `foreverz.cn` 目录创建 `main.go` 内输入如下内容，并使用 `go mod init` 和 `go mod tidy` 安装依赖：

```go
package main

import (
	"fmt"

	"github.com/valyala/fasthttp"
)

func main() {
	fasthttp.ListenAndServeTLS(":6789", "foreverz.cn.cert.pem", "foreverz.cn.key.pem", requestHandler)
}

func requestHandler(ctx *fasthttp.RequestCtx) {
	fmt.Printf("%s %s %s %s\n", ctx.URI().Scheme(), ctx.Method(), ctx.Host(), ctx.Path())
	ctx.SetBodyString(fmt.Sprintf("hello from %s!", ctx.URI().Scheme()))
}
```

运行 `go run main.go` 启动服务。

使用 cURL 测试服务：

```
$ curl https://foreverz.cn:6789/123 --resolve "foreverz.cn:6789:127.0.0.1"
```

如果此时没有安装根证书到系统，会报错，证书不合法：

```
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```

可以使用如下命令安装证书到系统(MacOS)：

```shell
$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./intermediate/ca-chain.cert.pem
```

此时再运行 cURL 命令，控制台显示 `hello from https!` 说明成功了。

PS：可以只安装中级CA证书，也可以安装证书链，只安装根CA证书不可以，会认证失败！

为了安全，在使用完成后尽量删除证书避免安全问题：

```shell
$ sudo security remove-trusted-cert -d intermediate/intermediate.cert.pem 
```

完结，撒花🎉🎉🎉


# 参考

1. [椭圆曲线密码学在OpenSSL中的实现](https://solobearchn.github.io/2018/03/18/ECC/)
2. [写给开发人员的实用密码学（七）—— 非对称密钥加密算法 RSA/ECC](https://thiscute.world/posts/practical-cryptography-basics-7-asymmetric-key-ciphers/)
3. [OpenSSL命令---ecparam](https://blog.csdn.net/as3luyuan123/article/details/14406429)
4. [Openssl ecparam命令](https://www.cnblogs.com/274914765qq/p/4668712.html)
5. [Adding trusted root certificates to the server](https://manuals.gfi.com/en/kerio/connect/content/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html)
