Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Single-Use Types — Cryptographic Guarantees via Ownership 🟡
单次使用类型:通过所有权获得密码学保证 🟡

What you’ll learn: How Rust’s move semantics act as a linear type system, making nonce reuse, double key-agreement, and accidental fuse re-programming impossible at compile time.
本章将学到什么: Rust 的移动语义怎样像线性类型系统一样工作,从而在编译期杜绝 nonce 复用、重复密钥协商,以及误重复烧录 fuse 这类问题。

Cross-references: ch01 (philosophy), ch04 (capability tokens), ch05 (type-state), ch14 (testing compile-fail)
交叉阅读: ch01 讲理念,ch04 讲能力令牌,ch05 讲类型状态,ch14 讲 compile-fail 测试。

The Nonce Reuse Catastrophe
Nonce 复用灾难

In authenticated encryption (AES-GCM, ChaCha20-Poly1305), reusing a nonce with the same key is catastrophic — it leaks the XOR of two plaintexts and often the authentication key itself. This isn’t a theoretical concern:
在认证加密算法里,比如 AES-GCM、ChaCha20-Poly1305,如果同一把密钥重复使用同一个 nonce,后果是灾难性的。它会泄露两份明文的异或结果,很多情况下连认证密钥本身都会被拖出来。这不是纸上谈兵:

  • 2016: Forbidden Attack on AES-GCM in TLS — nonce reuse allowed plaintext recovery
    2016 年:TLS 里的 AES-GCM 遭到 Forbidden Attack,nonce 复用导致明文可恢复
  • 2020: Multiple IoT firmware update systems found reusing nonces due to poor RNG
    2020 年:多个 IoT 固件升级系统因为随机数生成器太烂,出现了 nonce 重复使用

In C/C++, a nonce is just a uint8_t[12]. Nothing prevents you from using it twice.
在 C/C++ 里,nonce 本质上就是一个 uint8_t[12]。语言本身完全拦不住第二次使用。

// C — nothing stops nonce reuse
uint8_t nonce[12];
generate_nonce(nonce);
encrypt(key, nonce, msg1, out1);   // ✅ first use
encrypt(key, nonce, msg2, out2);   // 🐛 CATASTROPHIC: same nonce

Move Semantics as Linear Types
把移动语义看成线性类型

Rust’s ownership system is effectively a linear type system — a value can be used exactly once (moved) unless it implements Copy. The ring crate exploits this:
Rust 的所有权系统,本质上就很像一个线性类型系统。除非一个值实现了 Copy,否则它默认只能被使用一次,也就是被 move 一次。ring 这个库就把这一点用得很狠:

// ring::aead::Nonce is:
// - NOT Clone
// - NOT Copy
// - Consumed by value when used
pub struct Nonce(/* private */);

impl Nonce {
    pub fn try_assume_unique_for_key(value: &[u8]) -> Result<Self, Unspecified> {
        // ...
    }
    // No Clone, no Copy — can only be used once
}

When you pass a Nonce to seal_in_place(), it moves:
Nonce 被传给 seal_in_place() 时,它会被移动进去:

// Pseudocode mirroring ring's API shape
fn seal_in_place(
    key: &SealingKey,
    nonce: Nonce,       // ← moved, not borrowed
    data: &mut Vec<u8>,
) -> Result<(), Error> {
    // ... encrypt data in place ...
    // nonce is consumed — cannot be used again
    Ok(())
}

Attempting to reuse it:
如果还想再用一次:

fn bad_encrypt(key: &SealingKey, data1: &mut Vec<u8>, data2: &mut Vec<u8>) {
    let nonce = Nonce::try_assume_unique_for_key(&[0u8; 12]).unwrap();
    // .unwrap() is safe — a 12-byte array is always a valid nonce.
    seal_in_place(key, nonce, data1).unwrap();  // ✅ nonce moved here
    // seal_in_place(key, nonce, data2).unwrap();
    //                    ^^^^^ ERROR: use of moved value ❌
}

The compiler proves that each nonce is used exactly once. No test required.
编译器会证明每个 nonce 只会被使用一次。根本用不着写测试来碰运气。

Case Study: ring’s Nonce
案例:ring 里的 Nonce 设计

The ring crate goes further with NonceSequence — a trait that generates nonces and is also non-cloneable:
ring 更进一步,又引入了 NonceSequence。这个 trait 负责生成 nonce,而且自己同样不能被克隆:

/// A sequence of unique nonces.
/// Not Clone — once bound to a key, cannot be duplicated.
pub trait NonceSequence {
    fn advance(&mut self) -> Result<Nonce, Unspecified>;
}

/// SealingKey wraps a NonceSequence — each seal() auto-advances.
pub struct SealingKey<N: NonceSequence> {
    key: UnboundKey,   // consumed during construction
    nonce_seq: N,
}

impl<N: NonceSequence> SealingKey<N> {
    pub fn new(key: UnboundKey, nonce_seq: N) -> Self {
        // UnboundKey is moved — can't be used for both sealing AND opening
        SealingKey { key, nonce_seq }
    }

    pub fn seal_in_place_append_tag(
        &mut self,       // &mut — exclusive access
        aad: Aad<&[u8]>,
        in_out: &mut Vec<u8>,
    ) -> Result<(), Unspecified> {
        let nonce = self.nonce_seq.advance()?; // auto-generate unique nonce
        // ... encrypt with nonce ...
        Ok(())
    }
}
pub struct UnboundKey;
pub struct Aad<T>(T);
pub struct Unspecified;

The ownership chain prevents:
这一整条所有权链可以同时防住:

  1. Nonce reuseNonce is not Clone, consumed on each call
    Nonce 复用Nonce 不能 Clone,每次调用都会被消费掉
  2. Key duplicationUnboundKey is moved into SealingKey, can’t also make an OpeningKey
    密钥复制UnboundKey 会被 move 进 SealingKey,因此不能再同时拿去构造 OpeningKey
  3. Sequence duplicationNonceSequence is not Clone, so no two keys share a counter
    序列复制NonceSequence 不能 Clone,所以不会出现两把 key 共享同一个计数器

None of these require runtime checks. The compiler enforces all three.
这三件事都不需要运行时检查。 编译器已经提前把它们卡死了。

Case Study: Ephemeral Key Agreement
案例:一次性密钥协商

Ephemeral Diffie-Hellman keys must be used exactly once (that’s what “ephemeral” means). ring enforces this:
临时 Diffie-Hellman 私钥必须只使用一次,这就是 “ephemeral” 的含义。ring 也是这么做的:

/// An ephemeral private key. Not Clone, not Copy.
/// Consumed by agree_ephemeral().
pub struct EphemeralPrivateKey { /* ... */ }

/// Compute shared secret — consumes the private key.
pub fn agree_ephemeral(
    my_private_key: EphemeralPrivateKey,  // ← moved
    peer_public_key: &UnparsedPublicKey,
    error_value: Unspecified,
    kdf: impl FnOnce(&[u8]) -> Result<SharedSecret, Unspecified>,
) -> Result<SharedSecret, Unspecified> {
    // ... DH computation ...
    // my_private_key is consumed — can never be reused
    kdf(&[])
}
pub struct UnparsedPublicKey;
pub struct SharedSecret;
pub struct Unspecified;

After calling agree_ephemeral(), the private key no longer exists in memory (it’s been dropped). A C++ developer would need to remember to memset(key, 0, len) and hope the compiler doesn’t optimise it away. In Rust, the key is simply gone.
调用 agree_ephemeral() 之后,这把私钥在逻辑上就不再存在了,它已经被 drop 掉。C++ 开发者通常还得惦记着 memset(key, 0, len),然后再担心编译器会不会把这段清零优化掉。Rust 这边更干脆,值本身直接没了。

Hardware Application: One-Time Fuse Programming
硬件场景:一次性 Fuse 烧录

Server platforms have OTP (one-time programmable) fuses for security keys, board serial numbers, and feature bits. Writing a fuse is irreversible — doing it twice with different data bricks the board. This is a perfect fit for move semantics:
服务器平台里常见 OTP(一次性可编程)fuse,用来存安全密钥、板卡序列号、功能位。一旦写进去就回不了头。要是拿不同数据重复写,板子基本就废了。这种场景和移动语义简直是绝配。

use std::io;

/// A fuse write payload. Not Clone, not Copy.
/// Consumed when the fuse is programmed.
pub struct FusePayload {
    address: u32,
    data: Vec<u8>,
    // private constructor — only created via validated builder
}

/// Proof that the fuse programmer is in the correct state.
pub struct FuseController {
    /* hardware handle */
}

impl FuseController {
    /// Program a fuse — consumes the payload, preventing double-write.
    pub fn program(
        &mut self,
        payload: FusePayload,  // ← moved — can't be used twice
    ) -> io::Result<()> {
        // ... write to OTP hardware ...
        // payload is consumed — trying to program again with the same
        // payload is a compile error
        Ok(())
    }
}

/// Builder with validation — only way to create a FusePayload.
pub struct FusePayloadBuilder {
    address: Option<u32>,
    data: Option<Vec<u8>>,
}

impl FusePayloadBuilder {
    pub fn new() -> Self {
        FusePayloadBuilder { address: None, data: None }
    }

    pub fn address(mut self, addr: u32) -> Self {
        self.address = Some(addr);
        self
    }

    pub fn data(mut self, data: Vec<u8>) -> Self {
        self.data = Some(data);
        self
    }

    pub fn build(self) -> Result<FusePayload, &'static str> {
        let address = self.address.ok_or("address required")?;
        let data = self.data.ok_or("data required")?;
        if data.len() > 32 { return Err("fuse data too long"); }
        Ok(FusePayload { address, data })
    }
}

// Usage:
fn program_board_serial(ctrl: &mut FuseController) -> io::Result<()> {
    let payload = FusePayloadBuilder::new()
        .address(0x100)
        .data(b"SN12345678".to_vec())
        .build()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;

    ctrl.program(payload)?;      // ✅ payload consumed

    // ctrl.program(payload);    // ❌ ERROR: use of moved value
    //              ^^^^^^^ value used after move

    Ok(())
}

Hardware Application: Single-Use Calibration Token
硬件场景:一次性校准令牌

Some sensors require a calibration step that must happen exactly once per power cycle. A calibration token enforces this:
有些传感器要求每次上电之后必须做且只能做一次校准。这个时候就可以用一个校准令牌把流程卡住:

/// Issued once at power-on. Not Clone, not Copy.
pub struct CalibrationToken {
    _private: (),
}

pub struct SensorController {
    calibrated: bool,
}

impl SensorController {
    /// Called once at power-on — returns a calibration token.
    pub fn power_on() -> (Self, CalibrationToken) {
        (
            SensorController { calibrated: false },
            CalibrationToken { _private: () },
        )
    }

    /// Calibrate the sensor — consumes the token.
    pub fn calibrate(&mut self, _token: CalibrationToken) -> io::Result<()> {
        // ... run calibration sequence ...
        self.calibrated = true;
        Ok(())
    }

    /// Read a sensor — only meaningful after calibration.
    ///
    /// **Limitation:** The move-semantics guarantee is *partial*. The caller
    /// can `drop(cal_token)` without calling `calibrate()` — the token will
    /// be destroyed but calibration won't run. The `#[must_use]` annotation
    /// (see below) generates a warning but not a hard error.
    ///
    /// The runtime `self.calibrated` check here is the **safety net** for
    /// that gap. For a fully compile-time solution, see the type-state
    /// pattern in ch05 where `send_command()` only exists on `IpmiSession<Active>`.
    pub fn read(&self) -> io::Result<f64> {
        if !self.calibrated {
            return Err(io::Error::new(io::ErrorKind::Other, "not calibrated"));
        }
        Ok(25.0) // stub
    }
}

fn sensor_workflow() -> io::Result<()> {
    let (mut ctrl, cal_token) = SensorController::power_on();

    // Must use cal_token somewhere — it's not Copy, so dropping it
    // without consuming it generates a warning (or error with #[must_use])
    ctrl.calibrate(cal_token)?;

    // Now reads work:
    let temp = ctrl.read()?;
    println!("Temperature: {temp}°C");

    // Can't calibrate again — token was consumed:
    // ctrl.calibrate(cal_token);  // ❌ use of moved value

    Ok(())
}

When to Use Single-Use Types
什么时候适合用单次使用类型

Scenario
场景
Use single-use (move) semantics?
是否适合用单次使用的 move 语义
Cryptographic nonces
密码学 nonce
✅ Always — nonce reuse is catastrophic
✅ 永远要用,nonce 复用后果极其严重
Ephemeral keys (DH, ECDH)
临时密钥(DH、ECDH)
✅ Always — reuse weakens forward secrecy
✅ 永远要用,重复使用会削弱前向安全
OTP fuse writes
OTP fuse 烧录
✅ Always — double-write bricks hardware
✅ 永远要用,重复写可能直接把硬件写废
License activation codes
许可证激活码
✅ Usually — prevent double-activation
✅ 通常适合,用来防重复激活
Calibration tokens
校准令牌
✅ Usually — enforce once-per-session
✅ 通常适合,用来约束每个会话只做一次
File write handles
文件写句柄
⚠️ Sometimes — depends on protocol
⚠️ 有时适合,要看具体协议
Database transaction handles
数据库事务句柄
⚠️ Sometimes — commit/rollback is single-use
⚠️ 有时适合,因为 commit/rollback 本身就是单次行为
General data buffers
普通数据缓冲区
❌ These need reuse — use &mut [u8]
❌ 这类东西通常要反复使用,应该改用 &mut [u8]

Single-Use Ownership Flow
单次使用所有权流转图

flowchart LR
    N["Nonce::new()<br/>创建 nonce"] -->|move| E["encrypt(nonce, msg)<br/>加密时消费"]
    E -->|consumed| X["❌ nonce gone<br/>nonce 已经不存在"]
    N -.->|"reuse attempt<br/>尝试复用"| ERR["COMPILE ERROR:<br/>use of moved value"]
    style N fill:#e1f5fe,color:#000
    style E fill:#c8e6c9,color:#000
    style X fill:#ffcdd2,color:#000
    style ERR fill:#ffcdd2,color:#000

Exercise: Single-Use Firmware Signing Token
练习:单次使用的固件签名令牌

Design a SigningToken that can be used exactly once to sign a firmware image:
设计一个 SigningToken,它只能被使用一次,用来给固件镜像签名:

  • SigningToken::issue(key_id: &str) -> SigningToken (not Clone, not Copy)
    SigningToken::issue(key_id: &str) -> SigningToken,且它不能 Clone,也不能 Copy
  • sign(token: SigningToken, image: &[u8]) -> SignedImage (consumes the token)
    sign(token: SigningToken, image: &[u8]) -> SignedImage,签名时会消费掉 token
  • Attempting to sign twice should be a compile error.
    如果尝试签两次,应该在编译期报错。
Solution
参考答案
pub struct SigningToken {
    key_id: String,
    // NOT Clone, NOT Copy
}

pub struct SignedImage {
    pub signature: Vec<u8>,
    pub key_id: String,
}

impl SigningToken {
    pub fn issue(key_id: &str) -> Self {
        SigningToken { key_id: key_id.to_string() }
    }
}

pub fn sign(token: SigningToken, _image: &[u8]) -> SignedImage {
    // Token consumed by move — can't be reused
    SignedImage {
        signature: vec![0xDE, 0xAD],  // stub
        key_id: token.key_id,
    }
}

// ✅ Compiles:
// let tok = SigningToken::issue("release-key");
// let signed = sign(tok, &firmware_bytes);
//
// ❌ Compile error:
// let signed2 = sign(tok, &other_bytes);  // ERROR: use of moved value

Key Takeaways
本章要点

  1. Move = linear use — a non-Clone, non-Copy type can be consumed exactly once; the compiler enforces this.
    Move = 线性使用:一个既不 Clone 也不 Copy 的类型,天然就只能被消费一次,这一点由编译器保证。
  2. Nonce reuse is catastrophic — Rust’s ownership system prevents it structurally, not by discipline.
    Nonce 复用后果极其严重:Rust 通过所有权结构本身防住它,而不是靠人自觉。
  3. Pattern applies beyond crypto — OTP fuses, calibration tokens, audit entries — anything that must happen at most once.
    这套模式不只适用于密码学:OTP fuse、校准令牌、审计记录,凡是“最多只能发生一次”的东西都能套进去。
  4. Ephemeral keys get forward secrecy for free — the key agreement value is moved into the derived secret and vanishes.
    临时密钥天然有前向安全优势:密钥协商值被 move 进派生出的 secret 之后,就直接消失了。
  5. When in doubt, remove Clone — you can always add it later; removing it from a published API is a breaking change.
    拿不准时,先别给 Clone:后面要加很容易,等 API 发布以后再删掉 Clone 就成破坏性变更了。