分类 默认分类 下的文章

证书的生成和验证

1. 安装 OpenSSL

首先,确保您的系统上安装了 OpenSSL。在大多数 Linux 发行版中,您可以使用包管理器来安装它。

例如,在 Ubuntu 上:

sudo apt update
sudo apt install openssl

2. 生成私钥

私钥是安全证书的核心,应该保密。使用以下命令生成私钥:

openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048

这里,-algorithm RSA 指定了使用 RSA 算法,-out private.key 指定了输出文件名,rsa_keygen_bits:2048 设置了密钥长度。

3. 生成 CSR(证书签名请求)

CSR 是发送给证书颁发机构以获取签名证书的请求。生成 CSR 时,系统会提示您输入一些信息,如国家、州、组织等。

openssl req -new -key private.key -out certificate.csr

4. 生成自签名证书

在测试环境中,您可以使用 OpenSSL 生成自签名证书。以下是命令:

openssl x509 -req -days 365 -in certificate.csr -signkey private.key -out certificate.crt

这里,-days 365 设置了证书的有效期限为一年,-in certificate.csr 指定了 CSR 文件,-signkey private.key 指定了用于签名的私钥,-out certificate.crt 指定了输出证书文件。

5. 查看证书信息

您可以使用以下命令来查看证书的内容:

openssl x509 -in certificate.crt -text -noout

可以看到之前的设置内容:

 Signature Algorithm: sha256WithRSAEncryption
Issuer: C=CN, ST=SH, L=Shanghai, O=tianxiaohui.com, CN=10.236.90.154, emailAddress=eric@txh.com
Subject: C=CN, ST=SH, L=Shanghai, O=tianxiaohui.com, CN=10.236.90.154, emailAddress=eric@txh.com

6. 使用证书

生成的 certificate.crt(证书)和 private.key(私钥)可以用于配置 HTTPS 服务器,如 Apache 或 Nginx。

安装证书到 Flask server app

一个基于 Flask 到本地app:

from flask import Flask

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def index():
    return 'Hello, World!'
# ... 你的应用代码 ...


if __name__ == '__main__':
    # 指定证书和私钥的路径
    app.run(host='0.0.0.0', port=443, ssl_context=('certificate.crt', 'private.key'))

本地访问

本地浏览器访问的到下面的错误:
Your connection is not private
使用 python requests 访问:

requests.exceptions.SSLError: HTTPSConnectionPool(host='10.236.90.154', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1129)')))

非对称加密 RSA

使用 openssl 命令行工具生成RSA公钥和私钥

这个命令将生成一个2048位的RSA私钥,并将其保存在文件 private.pem 中

openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048

以下命令从私钥中提取公钥(public.pem):

openssl rsa -pubout -in private.pem -out public.pem

为什么可以从私钥中提取公钥?

钥文件通常包含了生成密钥对所需的所有信息,包括p、q、n、e和d。因此,可以从私钥文件中提取出n和e,这两者就构成了公钥

使用 python 生成 RSA 公钥和私钥

首先安装 cryptography 包.

 pip install cryptography

以下代码使用 cryptography 来生成 RSA 公钥和私钥.

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 生成私钥
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# 将私钥序列化为PEM格式
pem_private_key = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

# 从私钥中获取公钥
public_key = private_key.public_key()

# 将公钥序列化为PEM格式
pem_public_key = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# 打印私钥和公钥
print("Private Key (PEM format):")
print(pem_private_key.decode('utf-8'))

print("\nPublic Key (PEM format):")
print(pem_public_key.decode('utf-8'))

# 你可以将私钥和公钥保存到文件中
with open('private_key.pem', 'wb') as f:
    f.write(pem_private_key)

with open('public_key.pem', 'wb') as f:
    f.write(pem_public_key)

加密和解密

公钥加密消息:

# message.txt 是要加密的原始消息文件,encrypted_message.bin 是加密后的输出文件
openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out encrypted_message.bin

私钥解密消息:

# encrypted_message.bin 是加密后的消息文件,decrypted_message.txt 是解密后的输出文件
openssl rsautl -decrypt -in encrypted_message.bin -inkey private.pem -out decrypted_message.txt

签名和验证签名

签名涉及2步, 第一步对消息做哈希,生成 hash值, 第二步对 hash 值做加密(即签名).

python 版本签名.

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key

# 假设你已经有了私钥,这里我们使用之前生成的私钥
# load your private key from local file: private_key.pem
with open('private_key.pem', 'rb') as file:
    pem_private_key = file.read()

# 加载私钥
private_key = load_pem_private_key(pem_private_key, password=None, backend=default_backend())

# 要签名的消息
message = b"this is a message from https://www.tianxiaohui.com"

# 生成消息的哈希值
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(message)
digest_value = digest.finalize()

# 使用私钥对哈希值进行签名
signature = private_key.sign(
    digest_value,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)

# 打印签名
print("Signature:", signature)

# 使用公钥验证签名
# public_key = private_key.public_key()
with open('public_key.pem', 'rb') as file:
    public_key = load_pem_public_key(file.read())

try:
    public_key.verify(
        signature,
        digest_value,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print("Signature is valid.")
except:
    print("Signature is invalid.")

openssl 版本

# 生成RSA密钥对
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

# 创建一个文件进行签名
echo "This is a test message." > your_file.txt

# 使用私钥进行签名
openssl dgst -sha256 -sign private_key.pem -out signature.sig your_file.txt
# -sha256 表示使用SHA-256哈希算法。
# -sign private_key.pem 指定私钥文件。
# -out signature.sig 指定输出签名文件的名称。
# your_file.txt 是你要签名的文件。

# 使用公钥验证签名
openssl dgst -sha256 -verify public_key.pem -signature signature.sig your_file.txt

# -sha256 表示使用SHA-256哈希算法。
# -verify public_key.pem 指定公钥文件。
# -signature signature.sig 指定签名文件。
# your_file.txt 是你之前签名的文件。

公钥和私钥的格式

公钥和私钥可以以多种不同的格式保存,这些格式主要分为两大类:非加密格式和加密格式。以下是一些常见的密钥保存格式:

非加密格式

  1. PEM (Privacy Enhanced Mail) 格式:

    • 以文本形式存储,可以包含Base64编码的私钥或公钥。
    • 通常以 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----------BEGIN PRIVATE KEY----------END PRIVATE KEY----- 这样的标记开头和结尾。
  2. DER (Distinguished Encoding Rules) 格式:

    • 二进制格式,通常用于存储证书和密钥。
    • 不以文本形式存储,因此不能直接编辑。
  3. PKCS#1 格式:

    • 主要用于RSA密钥。
    • 可以是PEM编码的文本格式,也可以是DER格式的二进制格式。

加密格式

  1. PKCS#8 格式:

    • 专门用于私钥的存储,可以包含加密的私钥。
    • 可以是PEM编码的文本格式,也可以是DER格式的二进制格式。
    • 如果加密,通常会有 -----BEGIN ENCRYPTED PRIVATE KEY----------END ENCRYPTED PRIVATE KEY----- 标记。
  2. PKCS#12 (PFX) 格式:

    • 用于存储私钥、公钥和证书,通常包含一个密码保护的档案。
    • .pfx.p12 为文件扩展名。
    • 支持二进制格式,可以包含多个密钥和证书。

其他格式

  1. SSH 格式:

    • 专门用于SSH密钥。
    • 私钥通常以 .ssh/id_rsa 文件形式存在,公钥以 .ssh/id_rsa.pub 文件形式存在。
    • 私钥文件通常是PEM格式,但公钥文件是SSH专用的格式。
  2. XML 格式:

    • 少见,但某些应用程序可能会使用XML格式来存储密钥。

Python urllib self-signed certificate in certificate chain

从上周开始, 本地一个 Python 的项目里面一只报下面的错:

requests.exceptions.SSLError: (MaxRetryError("HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded with url: /bert-base-uncased/resolve/main/tokenizer_config.json (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)')))"), '(Request ID: 6d1db4c8-5589-4dfd-a14f-b95e47b9864d)')

从出错栈看, 当连接任何 https 网站的时候, 都会报这个错. 即便使用 pip install 的时候, 一样报这个错. 所以, 是本地连接任何 SSL 站点时候, 验证证书出错了. 由于连接任何 https 网站都有问题, 所以是本地的问题, 而不是各个 https 网站的问题.

Google 了一圈, 也没有找到解决方案. 但是有人提供了 truststore 这个包, 可以把当前操作系统的所有证书替换 Python 环境的, 如下代码, 可以暂时解决这个问题.

import truststore
truststore.inject_into_ssl()

后来发现, 其实使用 requests 包是不会出错的, 正常连接并返回. 可是某些不使用 requests 包的请求会出错.

import requests
print(requests.get("http://www.google.com")) # 一切正常

网上继续查, 很多人用下面的代码去验证使用的哪些 CA:

import certifi; print(certifi.where())

import _ssl; print(_ssl.get_default_verify_paths())

因为 Python 默认使用 openssl 的CA, 通过上面的 _ssl.get_default_verify_paths() 可以获得. 安装 certifi 因为它带了权威的 CA. 所以一般在安装完 Python 之后, 会有个脚本把 openssl 的 CA 指向 certifi 再带的证书. 详情: https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error

我这个问题, 即便把证书指向 certifi, 或者使用 certifi 生成 SSLContext 依然不能解决. 虽然不能解决, 但是发现它其实有个共同点, 就是都是用了 Python 自带的 urllib 来连接. 使用 requests 包来连接的都没问题.

看 urllib 有关这个问题的讨论, 发现有些人使用了 VPN 或者 代理, 但是我机器或代码并没有设置代理.

最终发现公司在所有人电脑上装了 Netskope, 这个东西相当于一个代理, 所有本地网络都会走它, 并且公司的软件中心有个 Netskope Certificates Apply Fix.

安装完这个之后, 我只想说 TMD. 耽误我8个多小时.

后续

后面发现事情的真相.

通常情况下, 我在公司的电脑访问 https://www.google.com 的证书是由 WR2 (Google Trust Services) 签发的.
但是, 由于安装了 Netskope, 那么它拦截了所有出入的请求, 现在访问 https://www.google.com, Google 的证书是由公司的根证书的二级证书签发的, 所以本地不包含公司CA的 CA 列表是无法认识这个由公司签发的证书的.

python flask 实现 SSE

这是最近2年第二次需要 server-sent events(SSE). 需求都是一样, 某个页面的需要后台去处理一个很长的请求, 后台处理至少要30秒钟. 这30多秒可以让用户去等, 但是有点长. 所以, 可以通过不断的把处理的进度和处理的中间结果尽快的展示到页面上显得尤为重要. 因此可以通过SSE把服务端到最新更新及时发送到页面, 让用户等等不是那么烦躁.

什么是 server-sent events

server-sent events 主要用来解决服务端要实时发送数据到客户端到情况. 比如最新的股票实时行情, 网页游戏服务端的实时数据, 视频/音频信息流等数据. 服务端随时可以推送数据到客户端, 客户端接收并处理. 但是客户端在第一次发送请求到服务端之后, 不会再发送新请求到服务端, 只能被动接收服务端到推送数据. 但是客户端可以选择随时关闭连接, 不再接收新数据. 这也是SSE 区分于 WebSockets 的一个重大区别.

server-sent events 客户端的接口

完全的文档可以看这里(https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events), 讲的很清楚.

客户端发起连接请求

const evtSource = new EventSource("//api.example.com/sse", {
  withCredentials: true,
});

客户端接收到匿名event

客户端处理:

evtSource.onmessage = (event) => {
  console.log(event);
};

这个时候, 服务端发送的数据是:

data: some text

# 或者是:
data: {"name": "eric", "age": 55}

客户端接收到命名event

客户端处理:

evtSource.addEventListener("myEventName", (event) => {
  console.log(event);
});

这个时候, 服务端发送的数据是:

event: myEventName
data: {"name": "eric", "age": 55}

客户端收到的注释(comment)

服务器可以发送以 : 开头的数据, 这时候客户端会认为这是没有任何意义的数据, 只是注释, 这通常只是用来保持这个连接, 不让它断掉. 比如

: this is a comment

服务端和客户端通信的协议

通过上面的例子可以看到, 2端其实都是纯文本行的数据发送与接收. 每行都是以 key: value 的形式表示的, 除了注释行, 它没有key, 是以 : 开头的一行.

允许的key 只有以下几种可能:

  1. event: event 的名字, 没有名字就默认是匿名事件.
  2. data: event的内容, 可以连续2行或多行都是 event: value 的形式, 这时候客户端浏览器会自动拼接2行或多行成一行, 拼接处加上换行符.
  3. id: event 的id, 可以没有.
  4. retry: 服务端发送给客户端, 当它侦测到断连之后, 多久才能发起重连断时间毫秒数.

所有不符合 key: value (这里的key是上面的4种) 和 : some comment 的形式, 都被认为是无效的.

客户端关闭连接

客户端通过下面的形式关闭连接:

evtSource.close();

客户端发现错误

客户端通过下面的代码做出错处理:

evtSource.onerror = (err) => {
  console.error("EventSource failed:", err);
};

关于客户端 EventSource 的所有API: https://developer.mozilla.org/en-US/docs/Web/API/EventSource

服务端的代码

其实客户端的代码相对来说比较统一, 浏览器都是统一规范. 服务端有不同的服务器语言, 实现起来却可能有差异. 比如 Java 的 JAX-RS 里面就有专门处理 SSE 的API.

今天我们就看一下 Python 里面是如何实现的.

python Flask 的实现

对于大家经常用到的 Flask, 如果你搜索 Flask SSE, 结果第一的是 https://flask-sse.readthedocs.io/en/latest/quickstart.html, 但是你点进去看, 发现它竟然使用 redis 去实现 SSE, 这相当于我又要安装一个 redis server. 这可都重的.

其实不需要这么实现, 也能达到 SSE 的效果. 服务端的代码如下:

import time
from flask import Flask, stream_with_context

@app.route('/sse')
def handle_sse_stream():
    def generate_event():
        while True:
           yield f'data: {"time": time.time()}\n\n'
           sleep(1000)
        
    return Response(stream_with_context(generate_event()), mimetype='text/event-stream')

上面的函数里面通过一个生成器, 不断的生成新的事件, 然后发送给客户端.

改进

通常我们的代码不会仅仅在那里sleep, 然后发送一个时间, 但是它可以每隔一段时间查询数据库去看最新的状态, 然后发送最新的状态给客户端. 比如上面的 yield 行的代码可以改成:

yield f'data: {"status": get_db_status()}\n\n'

进一步改进

上面的代码其实要求有一个数据库或者共享的组件来协调, 如果在同一个服务器上, 可以通过 queue.queue 这个队列来组成生产者/消费者 消息队列来传递消息.
上面的代码可以改成如下的方式:

def generate_event(queue):
    while True:
       yield f'data: {"msg": queue.get()}\n\n'
       sleep(1000)

这就解决了需要一个中间件来协调的问题.

总结

对于基于 Python flask 的 SSE, 其实可以通过 flask的 stream_with_context() 和 生成器来实现, 再通过 queue.queue 就实现了服务器内部的异步通信.

chrome 插件 SwitchyOmega 突然不能用 ERR_MANDATORY_PROXY_CONFIGURATION_FAILED

今天早上到公司, 任何网站都打不开了, 本地起的服务器本地端口都打不开. 出现如下页面:
not.png

症状

  1. 如果不用 SwitchyOmega 插件, 网站是可以打开的.
  2. 如果单单使用代理能打开墙外网站.
  3. 如果使用 Auto Switch 全都打不开.

我有一个墙外 proxy, 一个公司 PAC script. 然后使用前面2个组装一个 Auto Switch.

分析

根据症状分析 Auto Switch 里面的 公司 PAC script 出问题了. 于是检查 PAC script. 如下图:
php.png

竟然是一个 php 脚本, 这是???

说明公司的 PAC 脚本的服务器应该是 php 的, 它现在直接源文件返回了. 如果查看里面的php 内容, 发现它其实是产生 PAC 的脚本.

最后发现原来是公司这个 PAC 的服务器间歇性出问题, 有时候返回真正的 PAC, 有时候返回 php.