Zig 密码学与安全编程:AES、ChaCha20Poly1305 与 TLS

Zig 标准库内置了现代密码学原语模块 std.crypto,涵盖对称加密、AEAD、哈希、密钥派生、随机数生成等能力。本文系统讲解 AES-GCM 与 ChaCha20Poly1305 加解密、mbedtls-zig TLS 1.3 握手、Argon2/Scrypt 密钥派生、安全随机数生成与密码学最佳实践,帮助开发者构建安全可靠的 Zig 应用。

引言

密码学是现代应用安全的基石。Zig 的密码学能力来自两个层面:标准库 std.crypto 提供了经过严格审编的纯 Zig 实现(无外部 C 依赖),社区则提供了 mbedtls-zig、bearssl-zig 等成熟 C 库绑定,补充了 TLS/X.509 等上层协议支持。

与调用 OpenSSL 不同,Zig 的 std.crypto 模块使用固定大小数组和编译期类型检查,避免了许多 C 接口中的「缓冲区大小不匹配」问题。本文覆盖对称加解密(AES-GCM、ChaCha20Poly1305)、TLS 1.3 握手、密钥派生(Argon2、Scrypt)、随机数生成与密码学编程的安全红线。

前置:/zig-memory-management/(固定数组 vs 切片)、/zig-comptime-programming/(编译期类型约束)。


目录


1. std.crypto 概览

1.1 模块组织结构

std.crypto
  ├── aead          // AEAD 算法:GCM, ChaCha20Poly1305
  ├── aes           // AES 分组密码
  ├── chacha20      // ChaCha20 流密码
  ├── ecc           // 椭圆曲线
  ├── hash          // 哈希:Blake3, SHA256 等
  ├── kdf           // 密钥派生
  ├── pcurves       // 常用椭圆曲线
  ├── random        // CSPRNG 随机数
  └── tls           // TLS 1.3 客户端(实验性)

1.2 设计哲学

std.crypto 的核心设计特点:

  1. 无动态分配:所有接口使用固定大小的 [N]u8 数组,不依赖 Allocator。
  2. 编译期类型安全:密钥长度、nonce 长度是类型的一部分,编译时即检查。
  3. 纯 Zig 实现:无外部 C 依赖,可跨平台,可审计。
  4. 常量时间:关键比较操作(如 MAC 验证)使用 std.crypto.utils.timingSafeEql 防止时序攻击。

2. AES-GCM 对称加解密

AES-GCM(Galois/Counter Mode)是常用的 AEAD(Authenticated Encryption with Associated Data)算法,同时提供机密性和完整性。

2.1 加密

const std = @import("std");
const crypto = std.crypto;

pub fn aesGcmEncrypt(
    key: [32]u8,      // AES-256 密钥
    nonce: [12]u8,    // IV/Nonce,每个消息必须唯一
    plaintext: []const u8,
    associated_data: []const u8,
    ciphertext: []u8,
    tag: *[16]u8,
) void {
    // 密钥长度决定 AES-128/192/256
    var aes = crypto.aead.aes_gcm.Aes256Gcm.init(&key);
    aes.encrypt(ciphertext, tag, plaintext, associated_data, nonce);
}

参数解释:

  • key: 32 字节(256 位)对称密钥,可用随机数生成器安全生成。
  • nonce: 12 字节(96 位),每条消息必须唯一,重复使用会导致密钥恢复攻击。
  • associated_data: 不加密但参与 MAC 计算的数据(如协议头、时间戳)。
  • ciphertext: 输出缓冲区,长度 ≥ plaintext.len。
  • tag: 16 字节认证标签,解密时用来验证完整性。

2.2 解密与验证

pub fn aesGcmDecrypt(
    key: [32]u8,
    nonce: [12]u8,
    ciphertext: []const u8,
    associated_data: []const u8,
    tag: [16]u8,
    plaintext: []u8,
) !void {
    var aes = crypto.aead.aes_gcm.Aes256Gcm.init(&key);
    try aes.decrypt(plaintext, ciphertext, tag, associated_data, nonce);
    // decrypt 返回 error.InvalidAuthenticationTag(验证失败)
}

关键安全点:nonce 不可重用。实践中用自增计数器或随机生成。计数器方案需持久化存储防止重启后重复。


3. ChaCha20Poly1305:流式 AEAD

ChaCha20Poly1305 由 Daniel Bernstein 设计,被 TLS 1.3 列为强制算法,在纯软件实现中通常比 AES-GCM 更快(无 AES-NI 硬件时)。

3.1 完整加解密

const ChaCha20Poly1305 = std.crypto.aead.chacha_poly.XChaCha20Poly1305;

pub fn chachaEncrypt(
    key: [32]u8,
    nonce: [24]u8,      // XChaCha20 使用 192 位 nonce,重放风险极低
    plaintext: []const u8,
    ad: []const u8,
    ciphertext: []u8,
    tag: *[16]u8,
) void {
    var ctx = ChaCha20Poly1305.init(&key);
    ctx.encrypt(ciphertext, tag, plaintext, ad, nonce);
}

pub fn chachaDecrypt(
    key: [32]u8,
    nonce: [24]u8,
    ciphertext: []const u8,
    ad: []const u8,
    tag: [16]u8,
    plaintext: []u8,
) !void {
    var ctx = ChaCha20Poly1305.init(&key);
    try ctx.decrypt(plaintext, ciphertext, tag, ad, nonce);
}

3.2 XChaCha20Poly1305 vs ChaCha20Poly1305

变体Nonce 长度用途
ChaCha20Poly130512 字节(96 位)通信协议,每会话自增 nonce
XChaCha20Poly130524 字节(192 位)文件加密、随机 nonce 安全

XChaCha20Poly1305 的 192 位 nonce 允许安全使用随机 nonce,无需存储状态。推荐用于文件加密、数据库字段加密等「无状态」场景。


4. TLS 1.3 客户端与服务端

4.1 std.crypto.tls 客户端

Zig 标准库内置了 TLS 1.3 客户端(实验性),可与 std.net.Stream 配合使用:

const std = @import("std");
const tls = std.crypto.tls;

pub fn fetchHttps(allocator: std.mem.Allocator, hostname: []const u8) !void {
    // 建立 TCP 连接
    const stream = try std.net.tcpConnectToHost(allocator, hostname, 443);
    defer stream.close();

    // 升级为 TLS<br>    var client = try tls.Client.init(stream, .{
        .host = .{ .explicit = hostname },
        // 证书验证设置
        .ca_bundle = .{ .system = {} },
    });
    defer client.deinit();

    // 发送 HTTP GET 请求(TLS 加密传输)
    const request = "GET / HTTP/1.1\r\nHost: " ++ hostname ++ "\r\n\r\n";
    _ = try client.write(request);

    var buf: [4096]u8 = undefined;
    const n = try client.read(&buf);
    std.debug.print("{s}\n", .{buf[0..n]});
}

4.2 mbedtls-zig 绑定(生产推荐)

标准库的 std.crypto.tls 尚在发展,生产环境推荐 mbedtls-zig 或 bearssl-zig:

// 伪代码示意 mbedtls 绑定风格
const mbedtls = @cImport({
    @cInclude("mbedtls/ssl.h");
    @cInclude("mbedtls/net_sockets.h");
});

// 初始化 SSL 上下文
var ssl_ctx: mbedtls.mbedtls_ssl_context = undefined;
mbedtls.mbedtls_ssl_init(&ssl_ctx);

// 配置证书、握手、读写

mbedtls 提供了完整的 TLS 1.3 服务端/客户端、证书链验证、ALPN、SNI 等企业级功能,通过 @cImport 绑定后可在 Zig 中直接使用。


5. 密钥派生:Argon2 与 Scrypt

5.1 Argon2(密码哈希竞赛冠军)

Argon2 是密码哈希竞赛(Password Hashing Competition)的获胜算法,提供了抗 GPU/ASIC 攻击的能力。

const std = @import("std");
const crypto = std.crypto;

pub fn deriveKeyArgon2(
    password: []const u8,
    salt: [16]u8,
    params: crypto.pwhash.argon2.Params,
    out_key: []u8,
) !void {
    try crypto.pwhash.argon2.hash(
        out_key,
        password,
        salt,
        params,
        crypto.pwhash.argon2.Mode.argon2id,
    );
}

// 使用示例
pub fn main() !void {
    const password = "correct horse battery staple";
    var salt: [16]u8 = undefined;
    std.crypto.random.bytes(&salt); // 安全随机数

    var key: [32]u8 = undefined;
    try deriveKeyArgon2(password, salt, .{
        .t = 3,      // 迭代次数
        .m = 65536,  // 内存消耗 64 MB
        .p = 4,      // 并行度
    }, &key);

    std.debug.print("derived key: {x}\n", .{std.fmt.fmtSliceHexLower(&key)});
}

参数选择:

  • t(时间):迭代轮数,越高越慢但越安全。Web 登录场景通常 2-3 轮。
  • m(内存):KB 为单位的内存消耗。64 MB(65536)是折中选择;高安全场景可用 256 MB+。
  • p(并行度):线程数,不超过 CPU 核心数。

5.2 Scrypt(经典选择)

pub fn deriveKeyScrypt(
    password: []const u8,
    salt: [32]u8,
    log2_n: u6,      // CPU/内存成本参数(如 15 = 2^15 = 32768)
    r: u30,          // 块大小(通常 8)
    p: u30,          // 并行化因子(通常 1)
    out_key: []u8,
) !void {
    try crypto.pwhash.scrypt.kdf(allocator, out_key, password, salt, log2_n, r, p);
}

Scrypt 已被 Argon2 超越,但在已有系统中仍广泛使用。


6. 安全随机数生成

6.1 CSPRNG 使用

const std = @import("std");

pub fn main() !void {
    var buf: [32]u8 = undefined;

    // 使用 std.crypto.random(操作系统熵源 + CSPRNG)
    std.crypto.random.bytes(&buf);

    std.debug.print("random: {x}\n", .{std.fmt.fmtSliceHexLower(&buf)});
}

std.crypto.random 是线程安全的 CSPRNG,底层在 Linux 上读取 /dev/urandom,在 Windows 上调用 BCryptGenRandom,并可能使用 getrandom 系统调用。

6.2 密钥生成最佳实践

pub fn generateKey(comptime len: usize) [len]u8 {
    var key: [len]u8 = undefined;
    std.crypto.random.bytes(&key);
    return key;
}

// 生成各种长度的密钥
const aes256_key = generateKey(32);   // AES-256
const chacha_key = generateKey(32);   // ChaCha20
const hmac_key = generateKey(64);     // HMAC-SHA512

绝不使用 std.rand.DefaultPrng 进行密码学操作。DefaultPrng 是伪随机数生成器,不满足密码学安全要求。


7. 密码学编程安全红线

7.1 常见错误

错误后果正确做法
nonce 重用(GCM)密钥恢复,全密文解密计数器或随机 nonce
使用 DefaultPrng 生成密钥密钥可预测使用 std.crypto.random
忽略解密错误返回值接受伪造数据try decrypt 检查认证标签
密码直接当密钥字典攻击Argon2/Scrypt 密钥派生
明文传输密钥中间人窃取TLS 1.3 + 证书固定

7.2 常量时间比较

防止时序攻击:

const crypto = std.crypto;

pub fn verifyMac(expected: [16]u8, actual: [16]u8) bool {
    return crypto.utils.timingSafeEql([16]u8, expected, actual);
}

绝不能用 std.mem.eql 比较 MAC/签名:它会提前短路,泄露差异位置信息。


8. 速查表

需求函数/类型
AES-256-GCMcrypto.aead.aes_gcm.Aes256Gcm
XChaCha20Poly1305crypto.aead.chacha_poly.XChaCha20Poly1305
安全随机数std.crypto.random.bytes(&buf)
Argon2 密钥派生crypto.pwhash.argon2.hash(...)
常量时间比较crypto.utils.timingSafeEql
TLS 1.3 客户端std.crypto.tls.Client(实验性)
生产 TLSmbedtls-zig / bearssl-zig
哈希crypto.hash.blake2.Blake2b256 / crypto.hash.sha2.Sha256

9. 一句话记忆

std.crypto 纯 Zig 无动态分配:AES-GCM/XChaCha20 做 AEAD、Argon2 派生密钥、random.bytes 生成安全随机数、timingSafeEql 防时序攻击——TLS 1.3 标注库实验性,生产选 mbedtls-zig,nonce 绝不重用是红线。


相关阅读

  • /zig-memory-management/ — 固定数组与所有权
  • /zig-comptime-programming/ — 编译期类型约束
  • /zig-c-interoperability/ — C 库绑定(mbedtls 导入)

延伸阅读

  • /zig-http-server/ — HTTPS 服务与 TLS 配置
  • /zig-debugging-profiling/ — 安全审计与 Valgrind 检测
  • /zig-testing-quality/ — 密码学单元测试
  • [[zig]] — Zig 系统编程专题

// 完整示例:安全随机数 + XChaCha20Poly1305 加解密 + Argon2 密钥派生

const std = @import("std");
const crypto = std.crypto;
const XChaCha20Poly1305 = crypto.aead.chacha_poly.XChaCha20Poly1305;

pub fn main() !void {
    // ===== 1. 生成安全随机密钥和 nonce =====
    var key: [32]u8 = undefined;
    var nonce: [24]u8 = undefined;
    std.crypto.random.bytes(&key);
    std.crypto.random.bytes(&nonce);

    const plaintext = "Zig 密码学编程示例";
    var ciphertext: [64]u8 = undefined;
    var tag: [16]u8 = undefined;

    // ===== 2. 加密 =====
    var ctx = XChaCha20Poly1305.init(&key);
    ctx.encrypt(
        ciphertext[0..plaintext.len],
        &tag,
        plaintext,
        "associated-data",
        nonce,
    );
    std.debug.print("加密完成,tag = {x}\n", .{std.fmt.fmtSliceHexLower(&tag)});

    // ===== 3. 解密并验证 =====
    var decrypted: [64]u8 = undefined;
    try XChaCha20Poly1305.init(&key).decrypt(
        decrypted[0..plaintext.len],
        ciphertext[0..plaintext.len],
        tag,
        "associated-data",
        nonce,
    );
    std.debug.print("解密结果: {s}\n", .{decrypted[0..plaintext.len]});

    // ===== 4. Argon2 密钥派生演示 =====
    var salt: [16]u8 = undefined;
    std.crypto.random.bytes(&salt);
    var derived: [32]u8 = undefined;
    try crypto.pwhash.argon2.hash(
        &derived,
        "my-super-secret-password",
        salt,
        .{ .t = 3, .m = 65536, .p = 4 },
        .argon2id,
    );
    std.debug.print("派生密钥: {x}\n", .{std.fmt.fmtSliceHexLower(&derived)});
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「系统编程」更多文章

  1. Zig 可观测性:结构化日志、OpenTelemetry 与指标采集
  2. Zig WebSocket 与实时通信:服务端推送与帧解析
  3. Zig 数据库访问与轻量 ORM:SQLite、PostgreSQL 与自定义 SQL