HTTPS 通讯和证书

理解加密

假设客户端要给服务器发送:

password=123456

普通 HTTP 相当于直接发送:

客户端 ----------------------> 服务器

password=123456

如果有人抓包:

攻击者

     ↓

password=123456

内容可能直接被看到。

因此我们希望先把数据加密。

例如:

password=123456
       ↓
     加密
       ↓
7af91cd923....

服务器收到之后再解密。

问题是:

客户端和服务器到底使用什么密钥?

这就引出了现代 HTTPS 最核心的几个概念。


对称加密

最容易理解的加密方式就是:

同一把密钥

既可以加密,也可以解密。

例如:

密钥:

ABC123

客户端:

明文
 ↓
ABC123 加密
 ↓
密文

服务器:

密文
 ↓
ABC123 解密
 ↓
明文

这叫:

对称加密

常见算法包括:

AES
ChaCha20

它最大的优势是:

非常快。

所以真正的 HTTPS 数据传输阶段,大量数据主要使用的就是对称加密。

但是它存在一个严重问题。

客户端必须提前知道:

ABC123

服务器也必须知道:

ABC123

那么:

这把密钥最开始怎么安全地传过去?

如果直接通过网络发送:

客户端 ------ ABC123 ------> 服务器

攻击者只需要在中间截获一次:

ABC123

后面的通信就可能失去安全性。

因此还需要另外一种技术。


非对称加密

非对称加密不是只有一把密钥,而是一对密钥:

Private Key
私钥

Public Key
公钥

它们在数学上存在对应关系。

可以把它们暂时理解成:

公钥:可以公开
私钥:必须保密

例如服务器拥有:

server.key
    ↓
服务器私钥

server public key
    ↓
服务器公钥

其中:

私钥

只能保存在服务器。

而:

公钥

可以发送给任何客户端。

于是客户端可以利用服务器提供的信息建立安全通信。


现代 TLS 的实际密钥协商机制比“直接用公钥加密一个对称密钥”更加复杂,通常会使用类似 ECDHE 的密钥交换算法。

但在入门阶段可以先建立这样一个模型:

非对称密码体系
        ↓
帮助客户端和服务器安全建立共享秘密
        ↓
生成会话密钥
        ↓
后续大量数据使用对称加密

因此 HTTPS 并不是简单地:

所有数据全部使用 RSA 加密

而更接近:

TLS 握手
   ↓
身份认证 + 密钥协商
   ↓
得到 Session Key
   ↓
AES / ChaCha20 等对称算法
   ↓
高速加密后续 HTTP 数据

✳️ HTTPS通信大体过程

HTTPS通信包括:

浏览器服务器发起连接请求

服务器浏览器发送自己的数字证书。证书中包含一个公钥用来加密信息,注意这里 私钥由服务器秘密保存

浏览器 收到服务器的证书后,它会去验证这个数字证书到底是不是服务器的,数字证书有没有什么问题

验证方法就是 检查 颁发这个数字证书机构 签名是否正确。 颁发证书的机构会 说明这个网站的信息:包括名称、网址、证书有效期等。 然后 颁发机构会使用 自己的私钥对 这个信息进行 签名。 由于颁发机构的公钥都是 公布于众的, 可以在它的官方网站获取到,浏览器 可以用颁发机构公钥 来验证签名。

验证一致后,浏览器 会发送一个随机的字符串给服务器用私钥去加密,服务器把加密的结果返回给浏览器浏览器 用公钥解密这个返回结果,如果解密结果与之前生成的随机字符串一致,那说明对方确实是私钥的持有者,或者说对方确实是服务器

验证服务器的身份后,浏览器 再生成一个 对称加密算法和密钥,用于后面的通信的加密和解密。

这个对称加密算法和密钥,浏览器 会用公钥加密后发送给服务器,别人截获了也没用,因为只有服务器手中有可以解密的私钥。这样,后面服务器浏览器 就都可以用对称加密算法来加密和解密通信内容了。


大家可以发现 公私钥的加解密 只在连接建立的时候使用一下,用来验证服务端身份 和 加密传输 对称加密算法和密钥。

真正的数据传输会使用对称加密算法和另外的密钥进行。

数字证书 和 CA

✳️ 为何有了公钥还需要证书

这里又出现一个问题。

假设你访问:

https://git.company.local

服务器给你一个公钥:

Public Key A

你怎么知道:

Public Key A 真的是公司 GitLab 服务器的?

攻击者完全可以进行中间人攻击:

浏览器
   │
   ▼
攻击者
   │
   ▼
真正服务器

攻击者告诉浏览器:

这是 git.company.local 的公钥。

如果浏览器无条件相信,那么 HTTPS 仍然存在问题。

所以我们需要一种机制证明:

这个公钥

确实属于

git.company.local

这就是:

数字证书。



✳️ 数字证书是什么

可以把 HTTPS 证书 理解成服务器的:

数字身份证。

证书里面通常包含:

网站域名
公钥
证书颁发者
证书有效期
数字签名
证书用途
SAN 域名信息

例如:

Certificate

Subject:
    CN = git.company.local

SAN:
    DNS:git.company.local
    DNS:www.git.company.local

Public Key:
    ...

Issuer:
    Company Root CA

Valid:
    2026-08-01
       ↓
    2027-08-01

Signature:
    ...

这里最重要的内容之一就是:

Public Key

证书实际上把:

身份信息
+
公钥

绑定到了一起。


✳️ CA 到底是什么

CA 全称:

Certificate Authority

中文通常叫:

证书颁发机构

CA 的主要工作就是:

证明某个证书中的身份和公钥之间的关系。

例如:

Company Root CA
        │
        │ 签名
        ▼
git.company.local 证书

客户端如果相信:

Company Root CA

那么它就可以进一步相信:

由 Company Root CA 签发的证书

因此整个体系实际上是:

我不认识你的服务器
        │
        ▼
但我信任你的 CA
        │
        ▼
CA 证明你的服务器证书是真的
        │
        ▼
于是我信任你的服务器

这就是:

PKI

也就是 Public Key Infrastructure,公钥基础设施。


✳️ 浏览器为什么天然信任一些网站

打开操作系统或者浏览器的证书管理器,可以看到很多:

Root CA

例如一些商业证书机构的根证书。

这些证书在操作系统或者浏览器安装时就已经被加入:

Trusted Root Certification Authorities

也就是:

受信任的根证书颁发机构。

因此访问一个公网网站时,可能形成:

浏览器信任

Root CA
   │
   ▼
Intermediate CA
   │
   ▼
www.example.com

只要整个证书链可以一直追溯到浏览器已经信任的根 CA,浏览器就会认为:

证书可信


✳️ 根证书、中间证书和服务器证书

真实 PKI 中通常不是根 CA 直接给所有服务器签证书。

更常见的是:

Root CA
   │
   ▼
Intermediate CA
   │
   ▼
Server Certificate

即:

根 CA
   ↓
中间 CA
   ↓
网站证书

原因之一是:

根 CA 的私钥极其重要,不应该频繁用于日常签发。

大型企业一般也会采用类似结构:

Company Root CA
        │
        ▼
Company Internal Server CA
        │
        ├── git.company.local
        ├── jenkins.company.local
        ├── harbor.company.local
        └── api.company.local

✳️ 自签名证书

“自签名证书” 是服务器网站自己给自己签名发证书:

server.key
   │
   ▼
server.crt

也就是:

服务器证书

Issuer = 自己
Subject = 自己

这种证书浏览器默认不会信任。


✳️ 企业内部CA证书

企业内部更推荐的做法是:

自己建立 CA
       │
       ▼
CA 给内部服务器签证书

例如:

Company Root CA
      │
      ├── git.company.local
      ├── jenkins.company.local
      └── api.company.local

这虽然也是:

自建证书体系

但从 PKI 结构来看,比每台服务器自己生成一张自签名证书更加规范。

这种类型的证书,有的时候也被称作企业自签名证书。


整个证书体系中最重要的一条原则:

Certificate
可以公开

Private Key
绝对不能公开

例如:

company-root-ca.crt

可以安装在所有公司电脑。

但是:

company-root-ca.key

必须受到最高等级保护。

如果 Root CA 私钥泄露,攻击者理论上就能够签发:

假的 GitLab 证书
假的 Jenkins 证书
假的 API 证书

而公司电脑仍可能信任这些证书。

因此 CA 私钥泄露是:

整个内部 PKI 的严重安全事故。

证书体系中的个重要文件

学习 OpenSSL 时,经常会看到:

.key
.csr
.crt
.pem

这些文件非常容易混淆。

✳️ Private Key

例如:

server.key

这是:

私钥。

必须严格保密。


✳️ CSR

例如:

server.csr

全称:

Certificate Signing Request

也就是:

证书签名请求。

服务器生成:

私钥
+
CSR

然后把 CSR 交给 CA。

CSR 中通常包含:

公钥
域名
组织信息
SAN

但是:

CSR 中不包含服务器私钥。


✳️ Certificate

例如:

server.crt

这是 CA 最终签发的:

数字证书。


✳️ PEM

.pem 更多表示:

一种文本编码格式。

打开以后经常可以看到:

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

或者:

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

因此:

.pem

并不能单独说明它究竟是:

证书
还是
私钥

要看文件内部内容。

企业内部证书签发使用流程

创建自己的企业根 CA

首先建立目录:

mkdir company-ca
cd company-ca

生成 CA 私钥:

openssl genrsa -out company-root-ca.key 4096

这里:

company-root-ca.key

就是整个企业 CA 最重要的文件之一。

它可以用来:签发其他证书,因此必须严格保护。

然后创建根证书:

openssl req \
  -x509 \
  -new \
  -key company-root-ca.key \
  -sha256 \
  -days 3650 \
  -out company-root-ca.crt

过程中 OpenSSL 会要求填写:

Country
State
City
Organization
Organizational Unit
Common Name

例如:

Organization:

MyCompany

Common Name:

MyCompany Root CA

最终得到:

company-root-ca.key

company-root-ca.crt

两者关系:

company-root-ca.key
        │
        │ 私钥
        ▼

Company Root CA

        │
        ▼

company-root-ca.crt

其中:company-root-ca.key 绝对不能发给客户端。

客户端只需要:company-root-ca.crt

查看根证书内容

执行:

openssl x509 \
  -in company-root-ca.crt \
  -text \
  -noout

可以查看:

Issuer
Subject
Validity
Public Key
Signature Algorithm
Extensions

这是以后排查 HTTPS 问题非常重要的命令。

为内部网站生成私钥

假设企业内部网站是:

git.company.local

创建服务器私钥:

openssl genrsa \
  -out git.company.local.key \
  2048

现在服务器拥有:

git.company.local.key

这个文件必须:

只保存在服务器端。


创建服务器 CSR

执行:

openssl req \
  -new \
  -key git.company.local.key \
  -out git.company.local.csr

这里:

CSR

就是服务器向 CA 提交的:

证书申请。

可以查看 CSR:

openssl req \
  -in git.company.local.csr \
  -text \
  -noout

给服务器证书配置 SAN

证书颁发给的目标域名,以前主要使用这个字段:

CN
Common Name

例如:

CN=git.company.local

但是现代浏览器验证域名时,重点使用的是:

Subject Alternative Name

简称:

SAN

例如:

subjectAltName = DNS:git.company.local

如果一个证书需要支持多个域名:

DNS:git.company.local
DNS:git
DNS:git.internal.company.com

都应该放到 SAN 中。

如果直接使用 IP:

https://192.168.10.20

那么 SAN 中应该包含:

IP:192.168.10.20

而不是写成:

DNS:192.168.10.20

SAN信息是放在额外的一个文件中,我们创建:

git.ext

内容:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth

subjectAltName=@alt_names

[alt_names]
DNS.1=git.company.local
DNS.2=git

如果还需要通过 IP 访问:

IP.1=192.168.10.20

于是:

git.company.local
git
192.168.10.20

都可以写入同一张证书。


使用自己的 CA 签发服务器证书

执行:

openssl x509 \
  -req \
  -in git.company.local.csr \
  -CA company-root-ca.crt \
  -CAkey company-root-ca.key \
  -CAcreateserial \
  -out git.company.local.crt \
  -days 825 \
  -sha256 \
  -extfile git.ext

现在得到:

git.company.local.key
git.company.local.csr
git.company.local.crt

它们的作用分别是:

git.company.local.key
        ↓
服务器私钥

git.company.local.csr
        ↓
证书申请文件

git.company.local.crt
        ↓
CA 签发的服务器证书

验证证书是否由自己的 CA 签发

执行:

openssl verify \
  -CAfile company-root-ca.crt \
  git.company.local.crt

如果正确,会看到:

git.company.local.crt: OK

这实际上就是一次最简单的:

证书链验证。


使用 OpenSSL 查看服务器证书

执行:

openssl x509 \
  -in git.company.local.crt \
  -text \
  -noout

重点观察:

Issuer
Subject
Validity
Subject Alternative Name
Public Key
Key Usage
Extended Key Usage

尤其确认:

X509v3 Subject Alternative Name:

DNS:git.company.local

是否存在。


在 Nginx 中部署 HTTPS

假设已有:

git.company.local.crt

git.company.local.key

复制到:

/etc/nginx/ssl/

例如:

/etc/nginx/ssl/git.company.local.crt

/etc/nginx/ssl/git.company.local.key

然后配置 Nginx:

server {

    listen 443 ssl;

    server_name git.company.local;

    ssl_certificate
        /etc/nginx/ssl/git.company.local.crt;

    ssl_certificate_key
        /etc/nginx/ssl/git.company.local.key;

    location / {

        root /usr/share/nginx/html;

        index index.html;

    }

}

检查配置:

nginx -t

重新加载:

nginx -s reload

现在服务器已经可以:

HTTPS

通信。

但是浏览器此时仍然有可能报警。


为什么部署 HTTPS 后浏览器仍然报警

因为:

服务器有证书

并不代表:

客户端相信这个证书

浏览器会检查:

服务器证书
      │
      ▼
谁签发的?
      │
      ▼
Company Root CA
      │
      ▼
这个 CA 我信任吗?

如果操作系统没有安装:

company-root-ca.crt

那么浏览器会认为:

Unknown CA

所以还需要:

把企业 Root CA 安装到客户端的受信任根证书库。


Windows 信任企业根 CA

在 Windows 中打开:

certmgr.msc

进入:

Trusted Root Certification Authorities

↓

Certificates

导入:

company-root-ca.crt

也就是:

受信任的根证书颁发机构

注意:

客户端导入的是 CA 公共证书,不是 CA 私钥。

绝对不能把:

company-root-ca.key

复制给普通客户端。

重新打开浏览器以后访问:

https://git.company.local

如果:

域名
证书
证书链
有效期

都正确,浏览器就应该正常信任。


Linux 信任企业 CA

Debian / Ubuntu 系统通常可以:

sudo cp company-root-ca.crt \
  /usr/local/share/ca-certificates/company-root-ca.crt

然后:

sudo update-ca-certificates

系统就会把这个 CA 加入系统信任库。

之后:

curl https://git.company.local

也应该能够正常验证证书。


hosts 和内部 DNS

HTTPS 证书解决的是:

身份认证
+
加密

但它并不会自动解决:

域名解析

例如:

git.company.local

仍然需要解析到:

192.168.10.20

小型实验可以修改 hosts:

192.168.10.20 git.company.local

企业环境更合理的方式则是:

Internal DNS

例如:

git.company.local
        ↓
192.168.10.20

jenkins.company.local
        ↓
192.168.10.21

harbor.company.local
        ↓
192.168.10.22

于是整个企业内部形成:

Internal DNS
      +
Internal CA
      +
HTTPS

这才是一套相对完整的内部基础设施。


HTTPS 完整访问过程

现在可以把整个知识串起来。

用户输入:

https://git.company.local

首先进行 DNS 查询:

git.company.local
        ↓
192.168.10.20

浏览器连接:

192.168.10.20:443

服务器返回:

git.company.local 证书

浏览器检查:

域名是否匹配
       ↓
有效期是否正常
       ↓
用途是否正确
       ↓
证书签名是否正确
       ↓
CA 是否可信

浏览器发现:

Issuer:
Company Root CA

而:

Company Root CA

已经存在于本机:

Trusted Root CA Store

于是浏览器接受服务器身份。

TLS 随后完成:

密钥协商

生成本次通信使用的:

会话密钥

之后:

HTTP Request
HTTP Response

都会在 TLS 的保护下传输。


用一张图理解整个 HTTPS 信任体系

                  Company Root CA
                  ───────────────
                  Root CA Certificate
                  Root CA Private Key
                          │
                          │ 签名
                          ▼

                git.company.local
                ─────────────────
                  Server Certificate
                  Server Private Key
                          │
                          │
                          ▼

Windows / Linux / Browser
          │
          │ 信任
          ▼

    Company Root CA

          │
          │ 验证
          ▼

git.company.local Certificate

          │
          ▼

       TLS

          │
          ▼

        HTTPS

Docker 中部署内部 HTTPS

如果 Nginx 运行在 Docker 中,可以建立:

ssl/
├── git.company.local.crt
└── git.company.local.key

Compose:

services:

  nginx:

    image: nginx:latest

    ports:
      - "443:443"

    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro

这里:

证书
+
私钥

挂载到容器内部。

注意:

Docker 并不会改变 HTTPS 的原理。

本质仍然是:

Browser
   ↓
TCP 443
   ↓
Docker Port Mapping
   ↓
Nginx
   ↓
TLS Certificate

HTTPS 握手到底发生了什么

学习到这里以后,可以进一步介绍 TLS 握手。

可以将现代 TLS 过程简化为:

Client
   │
   │ ClientHello
   ▼
Server
   │
   │ ServerHello
   │ Certificate
   ▼
Client

浏览器验证:

Certificate

确认服务器身份。

随后双方进行:

Key Exchange

生成共同的:

Session Key

之后:

HTTP Request

HTTP Response

通过对称加密保护。

因此:

证书

主要参与:

身份认证

而真正大量 HTTP 数据并不是简单地全部使用证书中的公钥直接加密。