# nginx Multi-Certificates
Date: 2025-01-06


* * *

NGINX 配置多个证书一般有两种场景：

- 同一个域名下提供多种类型证书，如RSA，ECC证书
- 多个域名部署在同一个IP，使用多个不同证书， 根据客户端上送SNI选择证书

* * *

### **生成证书**

使用 ssl\_cert\_chain\_tool.sh 生成一组正式证书

```bash
RSA 类型证书
ssl_cert_chain_tool.sh -a RSA -o a_cert -n a.example.com -u
ssl_cert_chain_tool.sh -a RSA -o b_cert -n b.example.com -u
ssl_cert_chain_tool.sh -a RSA -o c_cert -n c.example.com -u
ECC 类型证书
ssl_cert_chain_tool.sh -a ECC -o d_cert -n d.example.com -u
ssl_cert_chain_tool.sh -a ECC -o e_cert -n e.example.com -u
```

* * *

### **支持多种类型证书**

```nginx
http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;
    keepalive_timeout 65;

    server {
        listen 0.0.0.0:443 ssl;
        ssl_protocols TLSv1.3 TLSv1.2;
        server_name a.example.com b.example.com c.example.com d.example.com e.example.com;

        ssl_prefer_server_ciphers on;

        # RSA and ECC Certificates
        ssl_certificate a_cert/server.cert.pem;
        ssl_certificate_key a_cert/server.key.pem;

        ssl_certificate e_cert/server.cert.pem;
        ssl_certificate_key e_cert/server.key.pem;

        ssl_certificate d_cert/server.cert.pem;
        ssl_certificate_key d_cert/server.key.pem;

        ssl_certificate c_cert/server.cert.pem;
        ssl_certificate_key c_cert/server.key.pem;

        ssl_certificate b_cert/server.cert.pem;
        ssl_certificate_key b_cert/server.key.pem;

        location / {
            return 200 'Multi-certificates test\n';
        }
    }
}

```

客户端与服务端握手时根据签名算法选择合适的证书， openssl 中使用tls\_post\_process\_client\_hello->tls\_choose\_sigalg 针对收到client hello报文后进行处理，上送的签名算法和服务端配置选择合适的证书。

```
openssl 使用状态机方式实现，由于要向前兼容要考虑很多中情况， 比如tls1.3 tls1.2兼容，如果套件中表明签名算法，从扩展栏位选， 扩展栏位没有从证书里找， 感觉维护起来都很复杂， 可以参考一下链接的源码分析，从中找到了一些线索。
https://hxndg.github.io/2020/09/07/OPENSSL%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB(3)/
```

tls1.2 套件包含了签名算法，指定RSA套件和扩展域 signature\_algorithms ， 会首先使用套件来优先选择对应的证书; tls1.3 套件剥离了签名算法，可以通过扩展字段 signature\_algorithms 来参与证书选择。

#### tls1.2的处理方式

```bash
# curl -ksv --tlsv1.2 --tls-max 1.2 --resolve a.example.com:443:127.0.0.1 https://a.example.com
* Added a.example.com:443:127.0.0.1 to DNS cache
* Hostname a.example.com was found in DNS cache
* Trying 127.0.0.1:443...
* Connected to a.example.com (127.0.0.1) port 443
* ALPN: curl offers http/1.1
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 / X25519 / id-ecPublicKey
* ALPN: server accepted http/1.1
* Server certificate:
* subject: C=CN; O=TestOrg; CN=d.example.com
```

可以看到他选择了ecc证书d\_cert, 如果指定套件可以看到, 则选择了RSA b\_cert,

```bash
# curl -ksv --tlsv1.2 --tls-max 1.2 --resolve a.example.com:443:127.0.0.1 https://a.example.com --ciphers ECDHE-RSA-AES128-GCM-SHA256
* Added a.example.com:443:127.0.0.1 to DNS cache
* Hostname a.example.com was found in DNS cache
* Trying 127.0.0.1:443...
* Connected to a.example.com (127.0.0.1) port 443
* ALPN: curl offers http/1.1
* Cipher selection: ECDHE-RSA-AES128-GCM-SHA256
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted http/1.1
* Server certificate:
* subject: C=CN; O=TestOrg; CN=b.example.com

```

可以看到tls1.2 套件起了的作用，如果设置了套件会用套件指定签名算法取选择证书， 而使用的扩展栏位是一样的。

```bash
Extension: signature_algorithms (len=42)
Type: signature_algorithms (13)
Length: 42
Signature Hash Algorithms Length: 40
Signature Hash Algorithms (20 algorithms)
Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
Signature Algorithm: ecdsa_secp384r1_sha384 (0x0503)
Signature Algorithm: ecdsa_secp521r1_sha512 (0x0603)
Signature Algorithm: ed25519 (0x0807)
Signature Algorithm: ed448 (0x0808)
Signature Algorithm: rsa_pss_pss_sha256 (0x0809)
Signature Algorithm: rsa_pss_pss_sha384 (0x080a)
Signature Algorithm: rsa_pss_pss_sha512 (0x080b)
Signature Algorithm: rsa_pss_rsae_sha256 (0x0804)
Signature Algorithm: rsa_pss_rsae_sha384 (0x0805)
Signature Algorithm: rsa_pss_rsae_sha512 (0x0806)
Signature Algorithm: rsa_pkcs1_sha256 (0x0401)
Signature Algorithm: rsa_pkcs1_sha384 (0x0501)
Signature Algorithm: rsa_pkcs1_sha512 (0x0601)
Signature Algorithm: SHA224 ECDSA (0x0303)
Signature Algorithm: SHA224 RSA (0x0301)
Signature Algorithm: SHA224 DSA (0x0302)
Signature Algorithm: SHA256 DSA (0x0402)
Signature Algorithm: SHA384 DSA (0x0502)
Signature Algorithm: SHA512 DSA (0x0602)

```

由于curl不能指定签名算法， 可以使用openssl sclient验证一下, tls1.2 协议指定签名算法。

```bash
# openssl s_client -connect 127.0.0.1:443 -sigalgs "RSA+SHA256" -tls1_2
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify return:1
---
Certificate chain
0 s:C = CN, O = TestOrg, CN = b.example.com
i:C = CN, O = TestOrg, CN = Intermediate CA
---
Server certificate
...
subject=C = CN, O = TestOrg, CN = b.example.com

issuer=C = CN, O = TestOrg, CN = Intermediate CA

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA
Server Temp Key: X25519, 253 bits
...
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
...
```

wireshark client hello

```bash
Extension: signature_algorithms (len=4)
Type: signature_algorithms (13)
Length: 4
Signature Hash Algorithms Length: 2
Signature Hash Algorithms (1 algorithm)
Signature Algorithm: rsa_pkcs1_sha256 (0x0401)
Signature Hash Algorithm Hash: SHA256 (4)
Signature Hash Algorithm Signature: RSA (1)

```

#### tls1.3的处理方式

如果使用Tls1.3 由于已经不支持RSA+SHA256, 这部分可以参考 RFC8446 https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.3， 虽然可以出现在整理里但是握手阶段已不支持， 可以使用 RSA-PSS+SHA256

```bash
# openssl s_client -connect 127.0.0.1:443 -sigalgs "RSA-PSS+SHA256"
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C = CN, O = TestOrg, CN = b.example.com
verify return:1
---
Certificate chain
0 s:C = CN, O = TestOrg, CN = b.example.com
i:C = CN, O = TestOrg, CN = Intermediate CA
---
Server certificate
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
subject=C = CN, O = TestOrg, CN = b.example.com

issuer=C = CN, O = TestOrg, CN = Intermediate CA

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1453 bytes and written 319 bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

```

关于为啥选择最后的证书这部分就不研究了，应该是类似stack结构后进先出， 主要是我们可以看到，其实并没有返回我想要的指定域名的证书。而是通过签名算法选择了证书, 如果要根据不同域名选择不同证书，并完成多站代理可以用第二种方法

### **SNI多证书配置**

nginx官方文档给的一个样例:

```nginx
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate www.example.com.crt;
...
}

server {
listen 443 ssl;
server_name www.example.org;
ssl_certificate www.example.org.crt;
...
}

```

https://nginx.org/en/docs/http/configuring\_https\_servers.html#certificate\_with\_several\_names

如果要将证书配置到同一个server通过 `map` 指令可以根据客户端提供的 SNI 动态加载对应的证书和私钥， 但是这样配置的话由于每次都要进行证书读取，IO消耗是比较大的， nginx plus 对于这种配置是有优化处理的 https://www.infoq.com/news/2019/04/nginx-plus-release-18/， 生产环境的话还是选择上一种配置。

**示例配置：**

```nginx
http {
    map $ssl_server_name $cert {
        a.example.com a_cert/server.cert.pem;
        b.example.com b_cert/server.cert.pem;
        d.example.com d_cert/server.cert.pem;
        e.example.com e_cert/server.cert.pem;
        default c_cert/server.cert.pem;
    }

    map $ssl_server_name $key {
        a.example.com a_cert/server.key.pem;
        b.example.com b_cert/server.key.pem;
        d.example.com d_cert/server.key.pem;
        e.example.com e_cert/server.key.pem;
        default c_cert/server.key.pem;
    }

    server {
        listen 0.0.0.0:443 ssl;
        ssl_protocols TLSv1.3 TLSv1.2;
        server_name a.example.com b.example.com c.example.com d.example.com e.example.com;

        ssl_certificate $cert;
        ssl_certificate_key $key;

        location / {
            return 200 'Multi-certificates test\n';
        }
    }
}
```

验证一下， 可以看到是通过server\_name选择证书a.example.com 选择 RSA 的a\_cert, e.example.com 选择ECC e\_cert

```bash
]# curl -ksv --resolve a.example.com:443:127.0.0.1 https://a.example.com
* Added a.example.com:443:127.0.0.1 to DNS cache
* Hostname a.example.com was found in DNS cache
* Trying 127.0.0.1:443...
* Connected to a.example.com (127.0.0.1) port 443
* ALPN: curl offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted http/1.1
* Server certificate:
* subject: C=CN; O=TestOrg; CN=a.example.com

curl -ksv --resolve e.example.com:443:127.0.0.1 https://e.example.com
* Added e.example.com:443:127.0.0.1 to DNS cache
* Hostname e.example.com was found in DNS cache
* Trying 127.0.0.1:443...
* Connected to e.example.com (127.0.0.1) port 443
* ALPN: curl offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey
* ALPN: server accepted http/1.1
* Server certificate:
* subject: C=CN; O=TestOrg; CN=e.example.com

```

### **最后**

关于多证书nginx用的就是这些，从上面看如果不是特殊需求nginx基本的配置能够满足， 在整理资料的时候在查看dh算法的时候偶然，看到了quantum-safe cryptographic algorithms.又是系列新的算法，后续有时间整理一下。

 

图片from[王進明](https://www.facebook.com/groups/474930279315210/user/100002793459548/?__cft__[0]=AZU54aBu3wVYtTeMkazP4A5_FEZPFBNhbO9Ke0V-7gOhdixOhg4-HPGZ9bJFIT3GgNYFNCSOpdKbQgveyCpSwlaj5nGk3418s7Y9RPqxuuC4aGeoAfTgQMFmTQU1cQlSOMXs3n8libhCr67Rbj_8uhPHgyiLQjvCPL6iOmeLrF3vaKZQbv2bZxg4LZdpYKMN9AM0H31dTLk9JzJ2fkvlPKBS&__tn__=-UC*F)

