diff --git a/Cargo.toml b/Cargo.toml index 11e1f13cc..9f7dbf738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } bouncycastle-sm3 = { path = "./crypto/sm3" } +bouncycastle-sm4 = { path = "./crypto/sm4" } bouncycastle-utils = { path = "./crypto/utils" } @@ -64,3 +65,4 @@ bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true bouncycastle-sm3.workspace = true +bouncycastle-sm4.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 548c4143e..16178129a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -16,6 +16,11 @@ mod rng_cmd; mod sha2_cmd; mod sha3_cmd; mod sm3_cmd; +mod sm4_cbc_cmd; +mod sm4_cfb8_cmd; +mod sm4_cfb_cmd; +mod sm4_ctr_cmd; +mod sm4_gcm_cmd; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; @@ -1696,6 +1701,228 @@ enum Subcommands { x: bool, }, + /// SM4 in CBC mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.2 mode) with PKCS7 + /// padding, streaming stdin to stdout. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two + /// compose directly in a pipeline. With `--iv` (hex) or `--iv-file` (raw bytes), the IV is + /// supplied instead: `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads + /// ONLY ciphertext from stdin. + /// + /// Input may be any length: it is padded with PKCS7 to a whole number of 16-byte blocks. + /// + /// See `aes128-cbc` for the full IV convention, the padding rule and the warnings; the key + /// and block are both 16 bytes, as for AES-128. + SM4_CBC { + #[arg(short, long)] + direction: CipherDirection, + + /// The 16-byte SM4 key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte SM4 key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// SM4 in CFB128 mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.3 mode), streaming + /// stdin to stdout. + /// + /// The segment size is the full block: draft-ribose-cfrg-sm4-10 calls this SM4-CFB-128. Its + /// 8-bit variant is a different, non-interoperable mode; use `sm4-cfb8` for that. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input. With `--iv` + /// (hex) or `--iv-file` (raw bytes), the IV is supplied instead and only ciphertext is written + /// or read. Input may be ANY length: nothing is padded. + /// + /// See `aes128-cfb` for the full IV convention, input-length rule and warnings; the key and + /// block are both 16 bytes, as for AES-128. + SM4_CFB { + #[arg(short, long)] + direction: CipherDirection, + + /// The 16-byte SM4 key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte SM4 key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// SM4 in CFB8 mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.3 mode, s = 8), + /// streaming stdin to stdout. + /// + /// The segment size is one byte. This is a DIFFERENT, NON-INTEROPERABLE mode from the CFB128 of + /// `sm4-cfb`: the two ciphertexts agree only on their first byte. It also costs one SM4 call + /// per byte, sixteen times the work of `sm4-cfb`, so prefer that unless a byte-granular + /// self-synchronising stream is required or the format demands CFB8. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input. With `--iv` + /// (hex) or `--iv-file` (raw bytes), the IV is supplied instead and only ciphertext is written + /// or read. Input may be ANY length: nothing is padded. + /// + /// See `aes128-cfb8` for the full IV convention, input-length rule and warnings; the key and + /// block are both 16 bytes, as for AES-128. + SM4_CFB8 { + #[arg(short, long)] + direction: CipherDirection, + + /// The 16-byte SM4 key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte SM4 key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 16-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// SM4 in CTR mode (GB/T 32907-2016 block cipher; NIST SP 800-38A Sec 6.5 mode), streaming + /// stdin to stdout. + /// + /// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one + /// message can be up to 2^32 blocks (64 GiB); past that the command errors rather than + /// repeating keystream. On `encrypt` the nonce is written as the FIRST 12 BYTES of the output + /// and on `decrypt` it is read back from there -- 12, not the 16 the other modes write. With + /// `--iv` (hex) or `--iv-file` (raw bytes), the 12-byte nonce is supplied instead and only + /// ciphertext is written or read; it MUST then be unique per message under one key. + /// + /// See `aes128-ctr` for the full nonce convention, input-length rule and warnings, including + /// why a repeated nonce is fatal and why CTR is the most malleable mode here; the key and + /// block are both 16 bytes, as for AES-128. + SM4_CTR { + #[arg(short, long)] + direction: CipherDirection, + + /// The 16-byte SM4 key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte SM4 key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Supplied instead of generated: `encrypt` then writes none + /// and `decrypt` reads none. See the nonce paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex + /// decoding). If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// SM4 in GCM (GB/T 32907-2016 block cipher; NIST SP 800-38D mode; RFC 8998 Sec 3.5.1's + /// AEAD_SM4_GCM), streaming stdin to stdout. + /// + /// AUTHENTICATED, unlike the other SM4 modes here: tampering with the ciphertext, the AAD or + /// the IV is detected rather than merely producing wrong plaintext. The framing is + /// `aes128-gcm`'s: on `encrypt` a fresh 12-byte IV is written first, the ciphertext follows + /// and the 16-byte tag comes last; `decrypt` reads the same layout back and checks the tag + /// once input is exhausted. With `--iv` / `--iv-file` the IV is supplied and not written or + /// read, and MUST then be unique per message under one key. `--aad` (hex) or `--aad-file` + /// (raw bytes) supply the additional authenticated data; if neither is given, AAD is empty. + /// + /// See `aes128-gcm` for the full contract and the warnings, including that on `decrypt` a tag + /// failure may be reported only after plaintext has already been written to stdout, so a + /// script MUST check the exit code; the key and block are both 16 bytes, as for AES-128. + SM4_GCM { + #[arg(short, long)] + direction: CipherDirection, + + /// The 16-byte SM4 key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte SM4 key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte IV in hex. Supplied instead of generated: `encrypt` then writes none and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. + #[arg(long)] + aad: Option, + + /// A file containing the additional authenticated data, as raw bytes (never hex-decoded). + /// If both aad and aad_file options are provided, the file will be used. + #[arg(long)] + aad_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given /// by repeated --element flags, each in hex; with none, stdin is hashed as a single element. /// The boundaries between elements are part of the hash. @@ -2033,6 +2260,21 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } + Some(Subcommands::SM4_CBC { direction, key, key_file, iv, iv_file, x }) => { + sm4_cbc_cmd::sm4_cbc_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::SM4_CFB { direction, key, key_file, iv, iv_file, x }) => { + sm4_cfb_cmd::sm4_cfb_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::SM4_CFB8 { direction, key, key_file, iv, iv_file, x }) => { + sm4_cfb8_cmd::sm4_cfb8_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::SM4_CTR { direction, key, key_file, iv, iv_file, x }) => { + sm4_ctr_cmd::sm4_ctr_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::SM4_GCM { direction, key, key_file, iv, iv_file, aad, aad_file, x }) => { + sm4_gcm_cmd::sm4_gcm_cmd(direction, key, key_file, iv, iv_file, aad, aad_file, *x); + } Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); } diff --git a/cli/src/sm4_cbc_cmd.rs b/cli/src/sm4_cbc_cmd.rs new file mode 100644 index 000000000..c5f192240 --- /dev/null +++ b/cli/src/sm4_cbc_cmd.rs @@ -0,0 +1,42 @@ +//! SM4-CBC encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are all +//! in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cbc` and `aes*-ecb` +//! commands. See that module for the command-line contract. Input is padded with PKCS7, so it +//! may be any length. +//! +//! `sm4-cbc` is the same command as `aes128-cbc` over the SM4 permutation (GB/T 32907-2016; +//! 16-byte key, 16-byte block), so every remark there applies unchanged. CBC (NIST SP 800-38A +//! Sec 6.2) provides confidentiality only: neither the ciphertext nor the IV is authenticated, +//! and a flipped ciphertext bit flips the same bit of the *next* block's plaintext (Appendix D). +//! Do not decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{ + CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, start_encryptor, +}; +use bouncycastle::cipher::modes::CBC; +use bouncycastle::cipher::padding::PKCS7; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key}; + +/// Dispatches to the shared streaming loops under the key's padded CBC mode. +pub(crate) fn sm4_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::>, KEY_LEN>(key, key_file, "SM4"); + let iv = load_iv::(iv, iv_file); + match action { + CipherDirection::Encrypt => { + let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng)); + encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None) + } + CipherDirection::Decrypt => { + decrypt_stream::<_, BLOCK_LEN>(iv, |iv| key.create_decryptor(iv), output_hex, None) + } + } +} diff --git a/cli/src/sm4_cfb8_cmd.rs b/cli/src/sm4_cfb8_cmd.rs new file mode 100644 index 000000000..acf4c733b --- /dev/null +++ b/cli/src/sm4_cfb8_cmd.rs @@ -0,0 +1,36 @@ +//! SM4-CFB8 encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and IV loaders), shared with the `aes*-cfb8` commands. See those modules for the +//! command-line contract. +//! +//! `sm4-cfb8` is the same command as `aes128-cfb8` over the SM4 permutation (GB/T 32907-2016; +//! 16-byte key, 16-byte block), so every remark there applies unchanged. The segment size is one +//! byte: this is draft-ribose-cfrg-sm4-10's **SM4-CFB-8** (Sec 8.5.1), a different and +//! non-interoperable mode from the SM4-CFB-128 of `sm4-cfb`, and it costs a full SM4 call per byte, +//! sixteen times the work. Prefer `sm4-cfb` unless a byte-granular self-synchronising stream is +//! required or the format demands CFB8. +//! +//! # Warning +//! +//! CFB8 provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. Do not decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key}; + +/// Dispatches to the shared streaming loops under the key's `CFB8` mode. +pub(crate) fn sm4_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "SM4"); + run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/sm4_cfb_cmd.rs b/cli/src/sm4_cfb_cmd.rs new file mode 100644 index 000000000..53fbe86ee --- /dev/null +++ b/cli/src/sm4_cfb_cmd.rs @@ -0,0 +1,40 @@ +//! SM4-CFB128 encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and IV loaders), shared with the `aes*-cfb` commands. See those modules for the +//! command-line contract. +//! +//! `sm4-cfb` is the same command as `aes128-cfb` over the SM4 permutation (GB/T 32907-2016; +//! 16-byte key, 16-byte block), so every remark there applies unchanged. The segment size is the +//! full block, i.e. draft-ribose-cfrg-sm4-10's **SM4-CFB-128** (Sec 8.5.1); the draft's 8-bit +//! variant is a different, non-interoperable mode and has its own command, `sm4-cfb8`. +//! +//! CFB is a stream cipher, so this command accepts input of any length and pads nothing; the +//! ciphertext is exactly as long as the plaintext. +//! +//! # Warning +//! +//! CFB provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. Flipping a ciphertext bit flips the *same* bit of the plaintext in the +//! *same* block (NIST SP 800-38A Appendix D, Table D.2), at the cost of randomising the next one, +//! so an attacker edits the block they aimed at. Do not decrypt data you have not authenticated +//! separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::sm4::{BLOCK_LEN, KEY_LEN, SM4Key}; + +/// Dispatches to the shared streaming loops under the key's `CFB` mode. +pub(crate) fn sm4_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "SM4"); + run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/sm4_ctr_cmd.rs b/cli/src/sm4_ctr_cmd.rs new file mode 100644 index 000000000..7a8c3ecc4 --- /dev/null +++ b/cli/src/sm4_ctr_cmd.rs @@ -0,0 +1,44 @@ +//! SM4-CTR encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the nonce convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and nonce loaders), shared with the `aes*-ctr` commands. See those modules for the +//! command-line contract. +//! +//! `sm4-ctr` is the same command as `aes128-ctr` over the SM4 permutation (GB/T 32907-2016; +//! 16-byte key, 16-byte block), so every remark there applies unchanged, including the split of +//! the counter block: the nonce is **12 bytes** and the counter the remaining 4, giving 2^32 +//! blocks -- 64 GiB -- in a single message. draft-ribose-cfrg-sm4-10 Sec 8.7 takes the counter +//! sequence as an input and requires only that it "does not repeat within the block size", so +//! this is one admissible choice; it is NIST SP 800-38A Appendix B.2's. +//! +//! `encrypt` writes that 12-byte nonce as the first bytes of its output and `decrypt` reads it +//! back, exactly as the other modes do with their IVs; note that it is 12 bytes here, not 16. +//! +//! # Warning +//! +//! CTR provides confidentiality only and is the most malleable mode here: flipping any ciphertext +//! bit flips exactly the corresponding plaintext bit and affects nothing else (SP 800-38A +//! Appendix D, Table D.2), so an attacker can edit the plaintext at will with no garbling to give +//! it away. A repeated nonce under one key is fatal rather than merely unwise -- the same keystream +//! twice leaks the XOR of the two messages -- which is why the nonce is drawn from the OS-backed +//! DRBG unless `--iv` / `--iv-file` supplies one, and then its uniqueness under the key is the +//! caller's to keep. Do not decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::sm4::{CTR_NONCE_LEN, KEY_LEN, SM4Key}; + +/// Dispatches to the shared streaming loops under the key's `CTR` mode. +pub(crate) fn sm4_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "SM4"); + run_stream_mode::<_, KEY_LEN, CTR_NONCE_LEN>(action, &key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/sm4_gcm_cmd.rs b/cli/src/sm4_gcm_cmd.rs new file mode 100644 index 000000000..d912bd6aa --- /dev/null +++ b/cli/src/sm4_gcm_cmd.rs @@ -0,0 +1,36 @@ +//! SM4-GCM authenticated encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV/tag framing, AAD loading and stdin streaming are in +//! [`crate::helpers::aead_cipher_helpers`], shared with the `aes*-gcm` commands. See that module +//! for the command-line contract (`IV || ciphertext || tag`, the AAD flags, and why a tag failure +//! may be reported after plaintext has already reached stdout). +//! +//! `sm4-gcm` is the same command as `aes128-gcm` over the SM4 permutation (GB/T 32907-2016; +//! 16-byte key, 16-byte block), which is RFC 8998 Sec 3.5.1's `AEAD_SM4_GCM`: a 12-byte nonce and +//! a 16-byte tag. Every remark there applies unchanged, above all that a repeated IV under one +//! key lets an attacker recover the hash subkey (SP 800-38D Appendix A). + +use crate::helpers::aead_cipher_helpers::{decrypt_gcm, encrypt_gcm, load_aad}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use bouncycastle::cipher::modes::{GCM, GCM_NONCE_LEN}; +use bouncycastle::sm4::{KEY_LEN, SM4Key}; + +/// Dispatches to the shared AEAD streaming loops with `Gcm`'s 128-bit tag. +pub(crate) fn sm4_gcm_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + aad: &Option, + aad_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "SM4"); + let nonce = load_iv::(iv, iv_file); + let aad = load_aad(aad, aad_file); + match action { + CipherDirection::Encrypt => encrypt_gcm::<_, KEY_LEN, 16>(&key, nonce, &aad, output_hex), + CipherDirection::Decrypt => decrypt_gcm::<_, KEY_LEN, 16>(&key, nonce, &aad, output_hex), + } +} diff --git a/cli/tests/sm4_cbc_cli_tests.rs b/cli/tests/sm4_cbc_cli_tests.rs new file mode 100644 index 000000000..f4120e07c --- /dev/null +++ b/cli/tests/sm4_cbc_cli_tests.rs @@ -0,0 +1,204 @@ +//! Tests for the `sm4-cbc` subcommand. +//! +//! `sm4-cbc` shares its key and IV loaders, streaming loops and error paths with the `aes*-cbc` +//! commands (`cli/src/helpers/block_mode_helpers.rs`), and `test_aes_cbc.sh` covers those +//! exhaustively. This file pins what is specific to SM4: the known-answer vectors in both +//! directions, the 16-byte key length, and that the command exists and round-trips. +//! +//! Vectors are the SM4-CBC examples of draft-ribose-cfrg-sm4-10 Appendix A.2.2. The appendix +//! applies no padding, so under the command's PKCS7 framing its two ciphertext blocks are +//! followed by one block of padding, computed here from the permutation. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// Appendix A.2 IV, shared by both CBC examples. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The two Appendix A.2 plaintext blocks. +const PLAINTEXT: &str = "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbb"; + +/// A.2.2.1 key and ciphertext. +const KEY_1: &str = "0123456789abcdeffedcba9876543210"; +const CT_1: &str = "78ebb11cc40b0a48312aaeb2040244cb4cb7016951909226979b0d15dc6a8f6d"; + +/// A.2.2.2 key and ciphertext. +const KEY_2: &str = "fedcba98765432100123456789abcdef"; +const CT_2: &str = "0d3a6ddc2d21c698857215587b7bb59a91f2c147911a4144665e1fa1d40bae38"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// The CBC ciphertext block that a full block of PKCS7 padding (sixteen `0x10` bytes) encrypts +/// to under `key_hex` after the ciphertext block `prev_hex`: `E_K(prev xor 10..10)`, from the +/// permutation. Appended to an unpadded known-answer ciphertext whose plaintext is +/// block-aligned, it gives the PKCS7 ciphertext of the same plaintext, which is how the command +/// frames it. +fn pkcs7_block(key_hex: &str, prev_hex: &str) -> String { + use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::sm4::hazmat::SM4Internal; + + let key_bytes: [u8; 16] = unhex(key_hex).try_into().expect("a 16-byte key"); + let material = KeyMaterial::<16>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + let sm4 = SM4Internal::new(&material).expect("a valid key"); + let mut block: [u8; 16] = unhex(prev_hex).try_into().expect("a 16-byte block"); + block.iter_mut().for_each(|b| *b ^= 0x10); + sm4.encrypt_block(&mut block); + tohex(&block) +} + +/// `encrypt --iv` reproduces the draft's ciphertext for both Appendix A.2.2 examples, followed +/// by the PKCS7 block, and `decrypt --iv` takes that back to the plaintext. +#[test] +fn encrypt_matches_appendix_a_2_2_vectors() { + for (section, key, ct) in [("A.2.2.1", KEY_1, CT_1), ("A.2.2.2", KEY_2, CT_2)] { + let out = + run_ok(&["sm4-cbc", "-d", "encrypt", "--key", key, "--iv", IV], &unhex(PLAINTEXT)); + let want = format!("{ct}{}", pkcs7_block(key, &ct[32..])); + assert_eq!(tohex(&out), want, "{section}: sm4-cbc encrypt --iv should give the ciphertext"); + + let back = run_ok(&["sm4-cbc", "-d", "decrypt", "--key", key, "--iv", IV], &out); + assert_eq!(tohex(&back), PLAINTEXT, "{section}: and decrypt --iv takes it back"); + } +} + +/// `decrypt` reproduces the draft's plaintext when handed the draft's IV followed by its +/// ciphertext and the PKCS7 block, for both Appendix A.2.2 examples. +#[test] +fn decrypt_matches_appendix_a_2_2_vectors() { + for (section, key, ct) in [("A.2.2.1", KEY_1, CT_1), ("A.2.2.2", KEY_2, CT_2)] { + let input = unhex(&format!("{IV}{ct}{}", pkcs7_block(key, &ct[32..]))); + let out = run_ok(&["sm4-cbc", "-d", "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{section}: sm4-cbc decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_1}{}", pkcs7_block(KEY_1, &CT_1[32..]))); + let out = run_ok(&["sm4-cbc", "-d", "decrypt", "--key", KEY_1, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the IV rides in the first block, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext: Vec = (0..4096u32).map(|i| (i.wrapping_mul(31) >> 3) as u8).collect(); + + let ciphertext = run_ok(&["sm4-cbc", "-d", "encrypt", "--key", KEY_1], &plaintext); + assert_eq!(ciphertext.len(), 16 + plaintext.len() + 16, "IV, the blocks, a padding block"); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&["sm4-cbc", "-d", "decrypt", "--key", KEY_1], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Any length is accepted and padded to the next whole block, as for the AES commands. +#[test] +fn any_input_length_is_accepted_and_padded() { + for len in [0usize, 1, 15, 16, 17, 33] { + let plaintext = vec![0x5Au8; len]; + let ciphertext = run_ok(&["sm4-cbc", "-d", "encrypt", "--key", KEY_1], &plaintext); + assert_eq!(ciphertext.len(), 16 + (len / 16 + 1) * 16, "len {len}: IV plus padded body"); + let recovered = run_ok(&["sm4-cbc", "-d", "decrypt", "--key", KEY_1], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// SM4 has exactly one key length. An AES-256-sized key is an error naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let key_256 = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + let err = run_err(&["sm4-cbc", "-d", "encrypt", "--key", key_256], &[0u8; 16]); + assert!(err.contains("SM4 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); +} + +/// The harness above must survive a write that loses the race with the child's exit. This pins it +/// deterministically: the key is rejected so `sm4-cbc` exits before reading a byte, and the payload +/// is far larger than any pipe buffer, so the write is certain to get EPIPE rather than merely +/// likely to. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let err = run_err( + &[ + "sm4-cbc", + "-d", + "encrypt", + "--key", + "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4", + ], + &vec![0u8; 4 * 1024 * 1024], + ); + assert!(err.contains("SM4 needs a 16-byte key"), "stderr was: {err}"); +} + +/// A ciphertext without PKCS7 padding is rejected: the bare A.2.2.1 ciphertext ends in a block +/// that does not unpad. +#[test] +fn a_ciphertext_without_pkcs7_padding_is_rejected() { + let input = unhex(&format!("{IV}{CT_1}")); + let err = run_err(&["sm4-cbc", "-d", "decrypt", "--key", KEY_1], &input); + assert!(err.contains("decryption failed"), "stderr was: {err}"); +} diff --git a/cli/tests/sm4_cfb8_cli_tests.rs b/cli/tests/sm4_cfb8_cli_tests.rs new file mode 100644 index 000000000..a8a91076c --- /dev/null +++ b/cli/tests/sm4_cfb8_cli_tests.rs @@ -0,0 +1,216 @@ +//! Tests for the `sm4-cfb8` subcommand. +//! +//! `sm4-cfb8` shares its key and IV loaders, streaming loops and error paths with the `aes*-cfb8` +//! commands (`cli/src/helpers/stream_mode_helpers.rs`), and `test_aes_cfb8.sh` covers those +//! exhaustively. This file pins what is specific to SM4: the 16-byte key length, that the command +//! exists and round-trips at any length, and that what comes out really is CFB8 over SM4 rather +//! than some other mode. +//! +//! draft-ribose-cfrg-sm4-10 defines SM4-CFB-8 (Sec 8.5.1) but publishes no example of it, so there +//! is no vector to decrypt here. Instead the command is required to agree with +//! `bouncycastle_cipher::modes::Cfb8` over the SM4 permutation, which +//! `crypto/sm4/tests/stream_mode_tests.rs` pins against the Sec 8.5.2 equations. That is the same +//! thing the AES CFB8 suite gets from SP 800-38A F.3, one step removed. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The single SM4 key length. +const KEY: &str = "0123456789abcdeffedcba9876543210"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} +/// The library's answer for the same key, IV and ciphertext, so the CLI can be held to it: the +/// key object's in-place decryption under `CFB8`. +fn library_cfb8_decrypt(key_hex: &str, iv: &[u8], data: &mut [u8]) { + use bouncycastle::cipher::modes::CFB8; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; + use bouncycastle::sm4::SM4Key; + + let key_bytes: [u8; 16] = unhex(key_hex).try_into().expect("a 16-byte key"); + let material = KeyMaterial::<16>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + let key = SM4Key::::from_key_material(&material).expect("a valid key"); + let iv: [u8; 16] = iv.try_into().expect("a 16-byte IV"); + key.decrypt_inplace(&iv, data).expect("decryption"); +} + +/// What the command emits must be exactly what `Cfb8` over `SM4` produces: take the IV the CLI +/// generated, hand the body to the library, and the plaintext must come back. +#[test] +fn the_command_is_cfb8_over_sm4() { + for len in [1usize, 5, 16, 17, 64] { + let plaintext = pseudo_random(len, len as u32); + let out = run_ok(&["sm4-cfb8", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(out.len(), 16 + len, "len {len}: IV block plus an equal-length body"); + + let (iv, body) = out.split_at(16); + let mut recovered = body.to_vec(); + library_cfb8_decrypt(KEY, iv, &mut recovered); + assert_eq!(recovered, plaintext, "len {len}: the CLI should agree with the library"); + } +} + +/// With `--iv` the output is the ciphertext alone, and it is still `Cfb8` over `SM4` under that +/// IV. +#[test] +fn a_supplied_iv_is_used_and_not_written() { + let iv = "000102030405060708090a0b0c0d0e0f"; + let plaintext = pseudo_random(37, 5); + let out = run_ok(&["sm4-cfb8", "-d", "encrypt", "--key", KEY, "--iv", iv], &plaintext); + assert_eq!(out.len(), plaintext.len(), "ciphertext only, no IV in band"); + + let mut recovered = out.clone(); + library_cfb8_decrypt(KEY, &unhex(iv), &mut recovered); + assert_eq!(recovered, plaintext, "the CLI should agree with the library under that IV"); + + let back = run_ok(&["sm4-cfb8", "-d", "decrypt", "--key", KEY, "--iv", iv], &out); + assert_eq!(back, plaintext); +} + +/// `encrypt | decrypt` composes: the IV rides in the first block, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + + let ciphertext = run_ok(&["sm4-cfb8", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(ciphertext.len(), 16 + plaintext.len(), "IV block plus an equal-length body"); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&["sm4-cfb8", "-d", "decrypt", "--key", KEY], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Any length is accepted and nothing is padded: CFB8's segment is a single byte. +#[test] +fn any_input_length_is_accepted() { + for len in [0usize, 1, 15, 17, 33] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["sm4-cfb8", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(ciphertext.len(), 16 + len, "len {len}: IV plus an equal-length body"); + let recovered = run_ok(&["sm4-cfb8", "-d", "decrypt", "--key", KEY], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// CFB8 and CFB128 are different modes, and `sm4-cfb8` must not be a second name for `sm4-cfb`: +/// handed the same IV and ciphertext, the two commands disagree past the first byte. +#[test] +fn sm4_cfb8_is_not_sm4_cfb() { + let iv = "000102030405060708090a0b0c0d0e0f"; + let body = "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbb"; + let input = unhex(&format!("{iv}{body}")); + + let as_cfb8 = run_ok(&["sm4-cfb8", "-d", "decrypt", "--key", KEY], &input); + let as_cfb = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", KEY], &input); + + assert_eq!( + as_cfb8[0], as_cfb[0], + "both XOR the first byte with MSB_8 of the same output block" + ); + assert_ne!(as_cfb8[1..], as_cfb[1..], "and they diverge from the second byte on"); +} + +/// SM4 has exactly one key length. An AES-256-sized key is an error naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let key_256 = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + let err = run_err(&["sm4-cfb8", "-d", "encrypt", "--key", key_256], &[0u8; 16]); + assert!(err.contains("SM4 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); +} + +/// The harness above must survive a write that loses the race with the child's exit. This pins it +/// deterministically: the key is rejected so `sm4-cfb8` exits before reading a byte, and +/// the payload is far larger than any pipe buffer, so the write is certain to get EPIPE rather +/// than merely likely to. It guards the `run` helper that every test in this file -- and, copy +/// for copy, the sibling cfb and ctr suites -- depends on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let err = run_err( + &[ + "sm4-cfb8", + "-d", + "encrypt", + "--key", + "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4", + ], + &vec![0u8; 4 * 1024 * 1024], + ); + assert!(err.contains("SM4 needs a 16-byte key"), "stderr was: {err}"); +} + +/// The subcommand is discoverable. +#[test] +fn the_subcommand_is_listed_in_help() { + let out = run_ok(&["--help"], &[]); + assert!(String::from_utf8_lossy(&out).contains("sm4-cfb8"), "`--help` should list sm4-cfb8"); +} diff --git a/cli/tests/sm4_cfb_cli_tests.rs b/cli/tests/sm4_cfb_cli_tests.rs new file mode 100644 index 000000000..c9ba2fc03 --- /dev/null +++ b/cli/tests/sm4_cfb_cli_tests.rs @@ -0,0 +1,172 @@ +//! Tests for the `sm4-cfb` subcommand. +//! +//! `sm4-cfb` shares its key and IV loaders, streaming loops and error paths with the `aes*-cfb` +//! commands (`cli/src/helpers/stream_mode_helpers.rs`), and `test_aes_cfb.sh` covers those +//! exhaustively. This file pins what is specific to SM4: the known-answer vectors in both +//! directions, the 16-byte key length, and that the command exists and round-trips at any length. +//! +//! Vectors are the SM4-CFB examples of draft-ribose-cfrg-sm4-10 Appendix A.2.4. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// Appendix A.2 IV, shared by both CFB examples. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The two Appendix A.2.4 plaintext blocks. +const PLAINTEXT: &str = "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbb"; + +/// A.2.4.1 key and ciphertext. +const KEY_1: &str = "0123456789abcdeffedcba9876543210"; +const CT_1: &str = "ac3236cb861dd316e6413b4e3c7524b769d4c54ed433b9a0346009beb37b2b3f"; + +/// A.2.4.2 key and ciphertext. +const KEY_2: &str = "fedcba98765432100123456789abcdef"; +const CT_2: &str = "5dcccd25a84ba16560d7f265887068490d9b86ff20c3bfe115ffa02ca6192cc5"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} +/// `encrypt --iv` reproduces the draft's ciphertext, and nothing but the ciphertext, for both +/// Appendix A.2.4 examples; `decrypt --iv` takes it back. +#[test] +fn encrypt_matches_appendix_a_2_4_vectors() { + for (section, key, ct) in [("A.2.4.1", KEY_1, CT_1), ("A.2.4.2", KEY_2, CT_2)] { + let out = + run_ok(&["sm4-cfb", "-d", "encrypt", "--key", key, "--iv", IV], &unhex(PLAINTEXT)); + assert_eq!(tohex(&out), ct, "{section}: sm4-cfb encrypt --iv should give the ciphertext"); + + let back = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", key, "--iv", IV], &out); + assert_eq!(tohex(&back), PLAINTEXT, "{section}: and decrypt --iv takes it back"); + } +} + +/// `decrypt` reproduces the draft's plaintext when handed the draft's IV followed by its +/// ciphertext, for both Appendix A.2.4 examples. +#[test] +fn decrypt_matches_appendix_a_2_4_vectors() { + for (section, key, ct) in [("A.2.4.1", KEY_1, CT_1), ("A.2.4.2", KEY_2, CT_2)] { + let input = unhex(&format!("{IV}{ct}")); + let out = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{section}: sm4-cfb decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_1}")); + let out = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", KEY_1, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the IV rides in the first block, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + + let ciphertext = run_ok(&["sm4-cfb", "-d", "encrypt", "--key", KEY_1], &plaintext); + assert_eq!(ciphertext.len(), 16 + plaintext.len(), "IV block plus an equal-length body"); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", KEY_1], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Unlike `sm4-cbc`, any length is accepted and nothing is padded: CFB is a stream cipher. +#[test] +fn any_input_length_is_accepted() { + for len in [0usize, 1, 15, 17, 33] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["sm4-cfb", "-d", "encrypt", "--key", KEY_1], &plaintext); + assert_eq!(ciphertext.len(), 16 + len, "len {len}: IV plus an equal-length body"); + let recovered = run_ok(&["sm4-cfb", "-d", "decrypt", "--key", KEY_1], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// SM4 has exactly one key length. An AES-256-sized key is an error naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let key_256 = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + let err = run_err(&["sm4-cfb", "-d", "encrypt", "--key", key_256], &[0u8; 16]); + assert!(err.contains("SM4 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); +} + +/// The subcommand is discoverable. +#[test] +fn the_subcommand_is_listed_in_help() { + let out = run_ok(&["--help"], &[]); + assert!(String::from_utf8_lossy(&out).contains("sm4-cfb"), "`--help` should list sm4-cfb"); +} diff --git a/cli/tests/sm4_ctr_cli_tests.rs b/cli/tests/sm4_ctr_cli_tests.rs new file mode 100644 index 000000000..f6284e31e --- /dev/null +++ b/cli/tests/sm4_ctr_cli_tests.rs @@ -0,0 +1,222 @@ +//! Tests for the `sm4-ctr` subcommand. +//! +//! `sm4-ctr` shares its key and nonce loaders, streaming loops and error paths with the +//! `aes*-ctr` commands (`cli/src/helpers/stream_mode_helpers.rs`), and `test_aes_ctr.sh` covers +//! those exhaustively. This file pins what is specific to SM4: the 16-byte key length, the +//! **12-byte** nonce, and that what comes out really is CTR over SM4. +//! +//! draft-ribose-cfrg-sm4-10's Appendix A.2.5 examples make the whole 16-byte IV the first counter +//! block, which the 12-byte-nonce split these commands use cannot produce (see the `SM4_CTR` docs), +//! so there is no vector to decrypt here. Instead the command is required to agree with +//! `bouncycastle_cipher::modes::Ctr` over the SM4 permutation, which +//! `crypto/sm4/tests/stream_mode_tests.rs` pins against the Sec 8.7.1 equations. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The single SM4 key length. +const KEY: &str = "0123456789abcdeffedcba9876543210"; + +/// CTR writes a 12-byte nonce, not the 16-byte IV the other modes write. +const NONCE_LEN: usize = 12; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} +/// The library's answer for the same key, nonce and ciphertext, so the CLI can be held to it: the +/// key object's in-place decryption under `CTR`. +fn library_ctr_decrypt(key_hex: &str, nonce: &[u8], data: &mut [u8]) { + use bouncycastle::cipher::modes::CTR; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; + use bouncycastle::sm4::SM4Key; + + let key_bytes: [u8; 16] = unhex(key_hex).try_into().expect("a 16-byte key"); + let material = KeyMaterial::<16>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + let key = SM4Key::::from_key_material(&material).expect("a valid key"); + let nonce: [u8; NONCE_LEN] = nonce.try_into().expect("a 12-byte nonce"); + key.decrypt_inplace(&nonce, data).expect("decryption"); +} + +/// What the command emits must be exactly what `Ctr` over `SM4` produces. +#[test] +fn the_command_is_ctr_over_sm4() { + for len in [1usize, 5, 16, 17, 64] { + let plaintext = pseudo_random(len, len as u32); + let out = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(out.len(), NONCE_LEN + len, "len {len}: nonce plus an equal-length body"); + + let (nonce, body) = out.split_at(NONCE_LEN); + let mut recovered = body.to_vec(); + library_ctr_decrypt(KEY, nonce, &mut recovered); + assert_eq!(recovered, plaintext, "len {len}: the CLI should agree with the library"); + } +} + +/// With `--iv` the 12-byte nonce is supplied, the output is the ciphertext alone, and it is still +/// `Ctr` over `SM4` under that nonce. +#[test] +fn a_supplied_nonce_is_used_and_not_written() { + let nonce = "000102030405060708090a0b"; + let plaintext = pseudo_random(37, 5); + let out = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY, "--iv", nonce], &plaintext); + assert_eq!(out.len(), plaintext.len(), "ciphertext only, no nonce in band"); + + let mut recovered = out.clone(); + library_ctr_decrypt(KEY, &unhex(nonce), &mut recovered); + assert_eq!(recovered, plaintext, "the CLI should agree with the library under that nonce"); + + let back = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY, "--iv", nonce], &out); + assert_eq!(back, plaintext); +} + +/// CTR writes a **12-byte** nonce where the other SM4 modes write a 16-byte IV, so the output is 12 +/// bytes longer than the input rather than 16. Getting this wrong would shift every byte of the +/// payload. +#[test] +fn the_nonce_is_twelve_bytes_not_sixteen() { + let plaintext = pseudo_random(64, 7); + let out = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(out.len(), plaintext.len() + NONCE_LEN, "a 12-byte nonce plus the ciphertext"); + + let via_cfb = run_ok(&["sm4-cfb", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(via_cfb.len(), plaintext.len() + 16, "sm4-cfb writes a 16-byte IV"); + + let back = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY], &out); + assert_eq!(back, plaintext); +} + +/// `encrypt | decrypt` composes at any length, with nothing padded. +#[test] +fn any_input_length_is_accepted_and_round_trips() { + for len in [0usize, 1, 15, 17, 33, 4096] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY], &plaintext); + assert_eq!(ciphertext.len(), NONCE_LEN + len, "len {len}: nonce plus an equal-length body"); + let recovered = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// A fresh nonce per invocation. For CTR this is the whole security argument: a repeated nonce +/// under one key repeats the keystream and leaks the XOR of the two messages. +#[test] +fn each_invocation_uses_a_fresh_nonce() { + let plaintext = pseudo_random(64, 11); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY], &plaintext); + assert!(seen.insert(ciphertext[..NONCE_LEN].to_vec()), "the CLI reused a nonce"); + let recovered = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let plaintext = pseudo_random(32, 3); + let ciphertext = run_ok(&["sm4-ctr", "-d", "encrypt", "--key", KEY], &plaintext); + + let binary = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY], &ciphertext); + let hex_out = run_ok(&["sm4-ctr", "-d", "decrypt", "--key", KEY, "-x"], &ciphertext); + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + + assert_eq!(hex_str, format!("{}\n", tohex(&binary))); + assert_eq!(binary, plaintext); +} + +/// SM4 has exactly one key length. An AES-256-sized key is an error naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let key_256 = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + let err = run_err(&["sm4-ctr", "-d", "encrypt", "--key", key_256], &[0u8; 16]); + assert!(err.contains("SM4 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); +} + +/// The subcommand is discoverable, and its help states the 12-byte nonce. +#[test] +fn the_subcommand_is_listed_in_help() { + let out = run_ok(&["--help"], &[]); + assert!(String::from_utf8_lossy(&out).contains("sm4-ctr"), "`--help` should list sm4-ctr"); + + let out = run_ok(&["sm4-ctr", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!( + help.contains("FIRST 12 BYTES") || help.contains("first 12 bytes"), + "help should say the nonce is 12 bytes: {help}" + ); +} diff --git a/cli/tests/sm4_gcm_cli_tests.rs b/cli/tests/sm4_gcm_cli_tests.rs new file mode 100644 index 000000000..748ab4cb2 --- /dev/null +++ b/cli/tests/sm4_gcm_cli_tests.rs @@ -0,0 +1,229 @@ +//! Tests for the `sm4-gcm` subcommand. +//! +//! `sm4-gcm` shares its key, IV and AAD loaders, streaming loops and error paths with the +//! `aes*-gcm` commands (`cli/src/helpers/aead_cipher_helpers.rs`), and `test_aes_gcm.sh` covers +//! those exhaustively. This file pins what is specific to SM4: the 16-byte key length, and that +//! what comes out really is GCM over SM4, held to RFC 8998 Appendix A.1's `AEAD_SM4_GCM` vector +//! and to the library's own `SM4Key`. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The single SM4 key length. +const KEY: &str = "0123456789abcdeffedcba9876543210"; + +/// GCM's framing: the 12-byte nonce leads and the 16-byte tag follows the ciphertext. +const NONCE_LEN: usize = 12; +const TAG_LEN: usize = 16; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} +/// The library's answer for the same key, nonce, AAD and inline `ciphertext || tag`, so the +/// CLI can be held to it: the key object's inline one-shot under `GCM`. +fn library_gcm_decrypt(key_hex: &str, nonce: &[u8], aad: &[u8], ciphertext: &[u8]) -> Vec { + use bouncycastle::cipher::modes::GCM; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::core::symmetric_key::{AeadDecryptionKey, FromKeyMaterial}; + use bouncycastle::sm4::SM4Key; + + let key_bytes: [u8; 16] = unhex(key_hex).try_into().expect("a 16-byte key"); + let material = KeyMaterial::<16>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + let key = SM4Key::::from_key_material(&material).expect("a valid key"); + let nonce: [u8; NONCE_LEN] = nonce.try_into().expect("a 12-byte nonce"); + key.decrypt_with_aad(&nonce, aad, ciphertext).expect("the tag verifies") +} + +/// RFC 8998 Appendix A.1, the `AEAD_SM4_GCM` vector, through the command with the vector's +/// nonce supplied: the output is exactly `ciphertext || tag`, and it decrypts back. +#[test] +fn the_command_reproduces_rfc_8998_a_1() { + const KEY_A1: &str = "0123456789ABCDEFFEDCBA9876543210"; + const NONCE_A1: &str = "00001234567800000000ABCD"; + const AAD_A1: &str = "FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2"; + const PT_A1: &str = concat!( + "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", + "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", + "EEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF", + "EEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAA", + ); + const CT_A1: &str = concat!( + "17F399F08C67D5EE19D0DC9969C4BB7D", + "5FD46FD3756489069157B282BB200735", + "D82710CA5C22F0CCFA7CBF93D496AC15", + "A56834CBCF98C397B4024A2691233B8D", + ); + const TAG_A1: &str = "83DE3541E4C2B58177E065A9BF7B62EC"; + + let args = ["sm4-gcm", "--key", KEY_A1, "--iv", NONCE_A1, "--aad", AAD_A1]; + let out = run_ok(&[&args[..], &["-d", "encrypt"]].concat(), &unhex(PT_A1)); + assert_eq!(tohex(&out), format!("{CT_A1}{TAG_A1}").to_lowercase()); + + let back = run_ok(&[&args[..], &["-d", "decrypt"]].concat(), &out); + assert_eq!(tohex(&back), PT_A1.to_lowercase()); +} + +/// What the command emits must be exactly what the library's `GCM` key produces: the generated +/// nonce leads, the body is as long as the input, and the tag follows. +#[test] +fn the_command_is_gcm_over_sm4() { + for len in [0usize, 1, 5, 16, 17, 64] { + let plaintext = pseudo_random(len, len as u32); + let out = + run_ok(&["sm4-gcm", "-d", "encrypt", "--key", KEY, "--aad", "deadbeef"], &plaintext); + assert_eq!(out.len(), NONCE_LEN + len + TAG_LEN, "len {len}: nonce, body and tag"); + + let (nonce, body) = out.split_at(NONCE_LEN); + let recovered = library_gcm_decrypt(KEY, nonce, &unhex("deadbeef"), body); + assert_eq!(recovered, plaintext, "len {len}: the CLI should agree with the library"); + + let back = run_ok(&["sm4-gcm", "-d", "decrypt", "--key", KEY, "--aad", "deadbeef"], &out); + assert_eq!(back, plaintext, "len {len}: round trip"); + } +} + +/// The AAD is covered by the tag: a different AAD, or none, on the decrypt side fails +/// authentication with a non-zero exit, and a flipped ciphertext or tag byte does too. +#[test] +fn tampering_and_a_changed_aad_fail_authentication() { + let plaintext = pseudo_random(69, 9); + let out = run_ok(&["sm4-gcm", "-d", "encrypt", "--key", KEY, "--aad", "deadbeef"], &plaintext); + + let err = run_err(&["sm4-gcm", "-d", "decrypt", "--key", KEY, "--aad", "00112233"], &out); + assert!(err.contains("authentication failed"), "stderr was: {err}"); + let err = run_err(&["sm4-gcm", "-d", "decrypt", "--key", KEY], &out); + assert!(err.contains("authentication failed"), "stderr was: {err}"); + + let mut body_flipped = out.clone(); + body_flipped[NONCE_LEN] ^= 1; + let err = + run_err(&["sm4-gcm", "-d", "decrypt", "--key", KEY, "--aad", "deadbeef"], &body_flipped); + assert!(err.contains("authentication failed"), "stderr was: {err}"); + + let mut tag_flipped = out.clone(); + let last = tag_flipped.len() - 1; + tag_flipped[last] ^= 1; + let err = + run_err(&["sm4-gcm", "-d", "decrypt", "--key", KEY, "--aad", "deadbeef"], &tag_flipped); + assert!(err.contains("authentication failed"), "stderr was: {err}"); +} + +/// A fresh nonce per invocation: a repeated GCM nonce under one key also gives away the hash +/// subkey, so this is the whole security argument for the default framing. +#[test] +fn each_invocation_uses_a_fresh_nonce() { + let plaintext = pseudo_random(64, 11); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..8 { + let ciphertext = run_ok(&["sm4-gcm", "-d", "encrypt", "--key", KEY], &plaintext); + assert!(seen.insert(ciphertext[..NONCE_LEN].to_vec()), "the CLI reused a nonce"); + let recovered = run_ok(&["sm4-gcm", "-d", "decrypt", "--key", KEY], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let plaintext = pseudo_random(32, 3); + let ciphertext = run_ok(&["sm4-gcm", "-d", "encrypt", "--key", KEY], &plaintext); + + let binary = run_ok(&["sm4-gcm", "-d", "decrypt", "--key", KEY], &ciphertext); + let hex_out = run_ok(&["sm4-gcm", "-d", "decrypt", "--key", KEY, "-x"], &ciphertext); + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + + assert_eq!(hex_str, format!("{}\n", tohex(&binary))); + assert_eq!(binary, plaintext); +} + +/// SM4 has exactly one key length. An AES-256-sized key is an error naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let key_256 = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + let err = run_err(&["sm4-gcm", "-d", "encrypt", "--key", key_256], &[0u8; 16]); + assert!(err.contains("SM4 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); +} + +/// The subcommand is discoverable, and its help says it authenticates and takes AAD. +#[test] +fn the_subcommand_is_listed_in_help() { + let out = run_ok(&["--help"], &[]); + assert!(String::from_utf8_lossy(&out).contains("sm4-gcm"), "`--help` should list sm4-gcm"); + + let out = run_ok(&["sm4-gcm", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.to_lowercase().contains("authenticat"), "help should mention authentication"); + assert!(help.contains("aad"), "help should mention AAD: {help}"); +} diff --git a/crypto/sm4/Cargo.toml b/crypto/sm4/Cargo.toml new file mode 100644 index 000000000..5f65bc2ce --- /dev/null +++ b/crypto/sm4/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bouncycastle-sm4" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +# The modes the key hands out, the `ModeNames` impl and the five `SM4_*` aliases; the engine +# itself does not use it. +bouncycastle-cipher.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "sm4_benches" +harness = false diff --git a/crypto/sm4/benches/sm4_benches.rs b/crypto/sm4/benches/sm4_benches.rs new file mode 100644 index 000000000..377ab4e42 --- /dev/null +++ b/crypto/sm4/benches/sm4_benches.rs @@ -0,0 +1,99 @@ +//! Criterion benchmarks for the bit-sliced SM4 engine. +//! +//! The comparison that matters is `encrypt_block` against `encrypt_2blocks` and `encrypt_4blocks` +//! over the same number of bytes. The S-box circuit always processes four lanes, so a single-block +//! call does four blocks' worth of work and the four-block path (what the CBC mode's decryption +//! uses) should be close to four times the throughput; the two-block path sits at half. Decryption +//! costs the same as encryption (the same loop with the round keys read backwards). + +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, KEY_LEN, LANES}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 1024 SM4 blocks. +const NUM_BLOCKS: usize = 1024; +const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; BLOCK_LEN]> { + let mut blocks = vec![[0u8; BLOCK_LEN]; NUM_BLOCKS]; + let mut generator = rng::DefaultRNG::default(); + for block in blocks.iter_mut() { + generator.next_bytes_out(block).unwrap(); + } + blocks +} + +fn key() -> KeyMaterial { + let mut bytes = [0u8; KEY_LEN]; + rng::DefaultRNG::default().next_bytes_out(&mut bytes).unwrap(); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("sm4::key expansion"); + + let key = key(); + group.bench_function("SM4Internal::new()", |b| { + b.iter(|| black_box(SM4Internal::new(black_box(&key)).unwrap())) + }); + + group.finish(); +} + +fn bench_sm4(c: &mut Criterion) { + let sm4 = SM4Internal::new(&key()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("sm4::SM4Internal"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + sm4.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_2blocks() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.as_chunks_mut::<2>().0 { + sm4.encrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_4blocks() x256", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for four in buf.as_chunks_mut::().0 { + sm4.encrypt_4blocks(black_box(four)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + sm4.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_sm4); +criterion_main!(benches); diff --git a/crypto/sm4/src/bitslice.rs b/crypto/sm4/src/bitslice.rs new file mode 100644 index 000000000..8c082a297 --- /dev/null +++ b/crypto/sm4/src/bitslice.rs @@ -0,0 +1,104 @@ +//! Bit-plane transposition for the constant-time S-box. +//! +//! # Layout +//! +//! The S-box is applied to the four bytes of one 32-bit word per round (`tau`, Sec 6.2.1). To +//! evaluate it as a Boolean circuit, four such words -- one per block, for four independent blocks +//! -- are split into 16-bit halves, eight words in all, and transposed so that plane `q[k]` holds +//! bit `k` of every one of the 16 bytes: +//! +//! ```text +//! before ortho: q[w] bit (8L + k) == bit k of byte L of half-word w +//! after ortho: q[k] bit (8L + w) == bit k of byte L of half-word w +//! ``` +//! +//! That is, within each byte-lane `L` of the eight half-words, the 8x8 bit matrix indexed by +//! (half-word, bit-within-lane) is transposed. Which byte of which word is "byte `L` of half-word +//! `w`" does not matter to the S-box, which is applied to every byte independently; only that the +//! same byte comes back to the same place, which [`ortho`] being its own inverse guarantees. +//! +//! `test_layout_matches_the_documented_table` pins the identity above exhaustively. +//! +//! # Why 16-bit planes +//! +//! Eight `u32` planes would hold one word from each of eight blocks and double the throughput on +//! a 32- or 64-bit machine, at the cost of doubling every buffer in the round: the working state +//! and the planes. This crate takes the smaller working set -- that is what "lowmemory" means +//! here -- and processes four blocks per pass. +//! +//! # Provenance +//! +//! The three-stage masked-swap transpose is the one `bouncycastle-aes` uses on `u32` words, +//! narrowed to `u16`: it transposes within each byte-lane, so the number of byte-lanes per word +//! does not enter into it. Both are translated from BearSSL `src/symcipher/aes_ct.c` +//! (`br_aes_ct_ortho`) by Thomas Pornin, MIT licensed. It is duplicated rather than shared +//! because it is each crate's private detail and neither should depend on the other. + +/// The eight bit-planes holding the two 16-bit halves of one word from each of four blocks. See +/// the module docs. +pub(crate) type Planes = [u16; 8]; + +/// Transposes bytes into bit-planes, and back -- it is its own inverse. +/// +/// Three stages of masked swaps exchange bit-fields of width 1, 2 and 4 between pairs of words, +/// which together transpose the 8x8 bit matrix inside each byte-lane. +pub(crate) fn ortho(q: &mut Planes) { + /// One masked swap: exchanges the `cl`-selected fields of `y` into `x` and the `ch`-selected + /// fields of `x` into `y`, moving them by `s` bit positions. + /// + /// `cl` and `ch` are complementary, and `s` is exactly the field width, so in each returned + /// word the two combined operands occupy disjoint bits: `(x & cl)` and `(y & cl) << s` cannot + /// both be set in the same position. `|` and `^` therefore compute the same function here, + /// which is why `cargo mutants` reports the `| -> ^` mutants in this function as surviving -- + /// they are equivalent programs. `test_ortho_is_an_involution` and + /// `test_layout_matches_the_documented_table` are what actually pin this code. + #[inline(always)] + fn swap(cl: u16, ch: u16, s: u32, x: u16, y: u16) -> (u16, u16) { + ((x & cl) | ((y & cl) << s), ((x & ch) >> s) | (y & ch)) + } + + // Stage 1: swap single bits between adjacent words (0x55 = even bits, 0xAA = odd bits). + for (a, b) in [(0, 1), (2, 3), (4, 5), (6, 7)] { + (q[a], q[b]) = swap(0x5555, 0xAAAA, 1, q[a], q[b]); + } + // Stage 2: swap 2-bit fields between words two apart. + for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] { + (q[a], q[b]) = swap(0x3333, 0xCCCC, 2, q[a], q[b]); + } + // Stage 3: swap nibbles between words four apart. + for (a, b) in [(0, 4), (1, 5), (2, 6), (3, 7)] { + (q[a], q[b]) = swap(0x0F0F, 0xF0F0, 4, q[a], q[b]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ortho_is_an_involution() { + let mut q: Planes = core::array::from_fn(|i| 0x0123u16.wrapping_mul(i as u16 * 37 + 3)); + let original = q; + ortho(&mut q); + assert_ne!(q, original, "ortho must actually move bits"); + ortho(&mut q); + assert_eq!(q, original); + } + + #[test] + fn test_layout_matches_the_documented_table() { + // A single set bit at (word w, byte-lane L, bit k) must land at (plane k, lane L, bit w). + for w in 0..8 { + for lane in 0..2 { + for k in 0..8 { + let mut q: Planes = [0; 8]; + q[w] = 1 << (8 * lane + k); + ortho(&mut q); + let mut expected: Planes = [0; 8]; + expected[k] = 1 << (8 * lane + w); + assert_eq!(q, expected, "word {w}, lane {lane}, bit {k}"); + } + } + } + } +} diff --git a/crypto/sm4/src/cbc.rs b/crypto/sm4/src/cbc.rs new file mode 100644 index 000000000..7e3860249 --- /dev/null +++ b/crypto/sm4/src/cbc.rs @@ -0,0 +1,90 @@ +//! Type alias for SM4 in CBC mode (NIST SP 800-38A Sec 6.2; draft-ribose-cfrg-sm4-10 Sec 8.4), +//! with padding. +//! +//! See `bouncycastle_cipher::modes::cbc` for the construction. The alias names the padded +//! workers, which accept input of any size; `NoPadding` accepts only whole blocks but goes through +//! the same adapter. The workers come from a key: [`SM4Key>`](crate::SM4Key) (or +//! `CBC`) hands them out through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair, and `SM4Key>` hands out the unpadded `Cbc` itself +//! through the block pair. +//! +//! CBC is defined only on whole blocks (SP 800-38A Sec 5.2), so CBC on data of any other length is +//! always CBC *plus a padding scheme*, and the scheme changes the ciphertext and must be agreed by +//! both ends. Naming it in the key's mode makes that choice explicit and makes a mismatched pair a +//! compile error. +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::>::from_key_material(&material).expect("a valid SM4 key"); +//! let message = b"a message of no particular length"; +//! +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, message).expect("encryption"); +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, message); +//! ``` +//! +//! With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +//! error rather than something silently padded: +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .unwrap(); +//! let key = SM4Key::>::from_key_material(&material).unwrap(); +//! +//! let mut out = [0u8; 16]; +//! let (written, _iv) = key.encrypt_out(&mut rng, &[0u8; 16], &mut out).expect("aligned"); +//! assert_eq!(written, 16); +//! assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::SM4Internal; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// SM4 in CBC mode with a padding scheme: what an [`SM4Key`](crate::SM4Key) under `CBC` +/// hands out. `Dir` is [`Encrypting`] or [`Decrypting`] and `Pad` is [`PKCS7`] or [`NoPadding`]; +/// the wrong direction is a compile error, not a runtime check. +#[allow(non_camel_case_types)] +pub type SM4_CBC<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor< + Cbc<'k, SM4Internal, Encrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc<'k, SM4Internal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, +>; diff --git a/crypto/sm4/src/cfb.rs b/crypto/sm4/src/cfb.rs new file mode 100644 index 000000000..f0143d098 --- /dev/null +++ b/crypto/sm4/src/cfb.rs @@ -0,0 +1,69 @@ +//! Type alias for SM4 in CFB mode (draft-ribose-cfrg-sm4-10 Sec 8.5; NIST SP 800-38A Sec 6.3), +//! full-block segment. +//! +//! See `bouncycastle_cipher::modes::cfb` for the construction. The alias is a stream cipher: the +//! data is a `&mut [u8]` of any length, encrypted or decrypted in place. It comes from a key, +//! [`SM4Key`](crate::SM4Key), through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair; the IV is one block, drawn from the caller's RNG by encryption +//! and returned, and there is no API for supplying one. +//! +//! The segment size is the full block, so this is the draft's **SM4-CFB-128**, one of the four +//! variants Sec 8.5.1 names: "SM4-CFB-1 ... SM4-CFB-8 ... SM4-CFB-64 ... SM4-CFB-128, the 128-bit +//! SM4-CFB mode, where s is set to 128." The `s = 8` variant is a different, non-interoperable mode +//! with its own alias, [`SM4_CFB8`](crate::SM4_CFB8); `s = 1` and `s = 64` are not implemented. +//! +//! Sec 8.5 says the plaintext "must be a multiple of the block size". `Cfb` accepts any length and +//! treats a short tail as a final segment of `s = 8r`, which is what every streaming CFB128 +//! implementation does; see "The final partial segment" in the `bouncycastle_cipher::modes::Cfb` +//! docs for why that is well defined. On whole blocks -- the only lengths the draft's Appendix +//! A.2.4 vectors cover -- the two readings coincide. +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CFB; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // 47 bytes: a stream cipher does not need a whole number of blocks. +//! let message = [0u8; 47]; +//! let mut data = message; +//! let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_ne!(data, message); +//! key.decrypt_inplace(&iv, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // Streaming, at any byte boundary: +//! let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); +//! let mut first = [0u8; 5]; +//! let mut rest = [1u8; 30]; +//! enc.do_encrypt_inplace(&mut first).unwrap(); +//! enc.do_encrypt_inplace(&mut rest).unwrap(); +//! let mut dec = key.create_decryptor(&iv); +//! dec.do_decrypt_inplace(&mut first).unwrap(); +//! dec.do_decrypt_inplace(&mut rest).unwrap(); +//! assert_eq!(first, [0u8; 5]); +//! assert_eq!(rest, [1u8; 30]); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::SM4Internal; +use bouncycastle_cipher::modes::Cfb; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// SM4 in CFB128 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +#[allow(non_camel_case_types)] +pub type SM4_CFB<'k, Dir> = Cfb<'k, SM4Internal, Dir, BLOCK_LEN>; diff --git a/crypto/sm4/src/cfb8.rs b/crypto/sm4/src/cfb8.rs new file mode 100644 index 000000000..7278ffef8 --- /dev/null +++ b/crypto/sm4/src/cfb8.rs @@ -0,0 +1,63 @@ +//! Type alias for SM4 in CFB8 mode (draft-ribose-cfrg-sm4-10 Sec 8.5 with `s = 8`; NIST +//! SP 800-38A Sec 6.3, `s = 8`). +//! +//! See `bouncycastle_cipher::modes::cfb8` for the construction. The alias is a stream cipher with +//! a one-byte segment, and comes from a key, [`SM4Key`](crate::SM4Key), through the +//! [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair; the IV is one block, drawn from +//! the caller's RNG by encryption and returned. +//! +//! This is the draft's **SM4-CFB-8**, named in Sec 8.5.1 as "the 8-bit SM4-CFB mode, where s is set +//! to 8". It is a **different, non-interoperable mode** from SM4-CFB-128, not a variant of it: +//! their ciphertexts differ from the second byte, and it costs a full SM4 call per byte, sixteen +//! times the work of [`SM4_CFB`](crate::SM4_CFB). See the `bouncycastle_cipher::modes::Cfb8` docs +//! for when that is the right trade. +//! +//! The draft defines the mode but publishes no example for it -- Appendix A.2.4's vectors are all +//! `s = 128` -- so `tests/stream_mode_tests.rs` pins this alias against the Sec 8.5.2 equations +//! evaluated directly over the permutation instead. +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::{CFB, CFB8}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let cfb8_key = SM4Key::::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // 5 bytes: CFB8's segment is one byte, so any length at all is fine. +//! let message = *b"hello"; +//! let mut data = message; +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_ne!(data, message); +//! cfb8_key.decrypt_inplace(&iv, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // SM4-CFB-8 and SM4-CFB-128 are not interchangeable: same key material, same IV, different +//! // ciphertext. +//! let cfb_key = SM4Key::::from_key_material(&material).unwrap(); +//! let mut as_cfb8 = message; +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut as_cfb8).unwrap(); +//! let mut as_cfb128 = as_cfb8; +//! cfb_key.decrypt_inplace(&iv, &mut as_cfb128).unwrap(); +//! assert_ne!(as_cfb128, message); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::SM4Internal; +use bouncycastle_cipher::modes::Cfb8; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// SM4 in CFB8 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +#[allow(non_camel_case_types)] +pub type SM4_CFB8<'k, Dir> = Cfb8<'k, SM4Internal, Dir, BLOCK_LEN>; diff --git a/crypto/sm4/src/ctr.rs b/crypto/sm4/src/ctr.rs new file mode 100644 index 000000000..dbf5cedbd --- /dev/null +++ b/crypto/sm4/src/ctr.rs @@ -0,0 +1,77 @@ +//! Type alias for SM4 in CTR mode (draft-ribose-cfrg-sm4-10 Sec 8.7; NIST SP 800-38A Sec 6.5). +//! +//! See `bouncycastle_cipher::modes::ctr` for the construction. The alias is a stream cipher: the +//! data is a `&mut [u8]` of any length, encrypted or decrypted in place. It comes from a key, +//! [`SM4Key`](crate::SM4Key), through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair; the nonce is drawn from the caller's RNG by encryption and +//! returned, and there is no API for supplying one. +//! +//! # The nonce length is 12, so the counter is 4 +//! +//! `Ctr` splits the counter block into a nonce and a counter by the length of its init data, and +//! this alias, like `CTR`'s default, chooses a **12-byte nonce**, leaving the 4-byte counter that +//! is the mode's maximum. That allows 2^32 blocks -- 64 GiB -- in one message, and past it the +//! mode errors rather than repeating keystream. A shorter message limit in exchange for more nonce +//! bits is available by naming `CTR<13>`, `CTR<14>` or `CTR<15>` as the key's mode. +//! +//! Sec 8.7 takes the counter sequence `T` as an input beside the nonce and requires only that "the +//! counter could be any sequence that does not repeat within the block size", so `nonce || counter` +//! is one admissible choice rather than a deviation; SP 800-38A Appendix B.2, which `Ctr` +//! implements, is the same construction spelled out. The draft's own Appendix A.2.5 examples take a +//! different one -- the whole 16-byte IV is `T_1` and the block is incremented as a unit -- which +//! this split cannot express, so those vectors drive a reference CTR over the permutation in +//! `tests/stream_mode_tests.rs` and the `CTR` key is checked against that reference. +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // 47 bytes: a stream cipher does not need a whole number of blocks. +//! let message = [0u8; 47]; +//! let mut data = message; +//! let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_eq!(nonce.len(), 12); +//! assert_ne!(data, message); +//! key.decrypt_inplace(&nonce, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // Streaming, at any byte boundary: +//! let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); +//! let mut first = [0u8; 5]; +//! let mut rest = [1u8; 30]; +//! enc.do_encrypt_inplace(&mut first).unwrap(); +//! enc.do_encrypt_inplace(&mut rest).unwrap(); +//! let mut dec = key.create_decryptor(&nonce); +//! dec.do_decrypt_inplace(&mut first).unwrap(); +//! dec.do_decrypt_inplace(&mut rest).unwrap(); +//! assert_eq!(first, [0u8; 5]); +//! assert_eq!(rest, [1u8; 30]); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::SM4Internal; +use bouncycastle_cipher::modes::Ctr; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// The nonce length this alias uses, leaving a 4-byte counter. +pub const CTR_NONCE_LEN: usize = 12; + +/// SM4 in CTR mode with a 12-byte nonce. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +#[allow(non_camel_case_types)] +pub type SM4_CTR<'k, Dir> = Ctr<'k, SM4Internal, Dir, BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/sm4/src/gcm.rs b/crypto/sm4/src/gcm.rs new file mode 100644 index 000000000..492a41d4e --- /dev/null +++ b/crypto/sm4/src/gcm.rs @@ -0,0 +1,82 @@ +//! Type alias for SM4 in GCM (NIST SP 800-38D over the SM4 permutation; RFC 8998 Sec 3.5.1's +//! `AEAD_SM4_GCM`). +//! +//! See `bouncycastle_cipher::modes::gcm` for the construction. The alias is an authenticated +//! cipher: encryption produces a tag as well as a ciphertext, and decryption either returns the +//! plaintext or fails the tag check. Both directions implement [`AEADCipherEncryptor`] / +//! [`AEADCipherDecryptor`], which keep the tag detached, and [`SymmetricCipherEncryptor`] / +//! [`SymmetricCipherDecryptor`], which carry it inline as `ciphertext || tag`. They come from a +//! key, [`SM4Key`](crate::SM4Key), through the [`AeadEncryptionKey`] / +//! [`AeadDecryptionKey`] pair, which draws the nonce from the caller's RNG and returns it, or +//! takes the caller's through its `_nonce` forms. `Dir` is [`Encrypting`] or [`Decrypting`]; the +//! wrong direction is a compile error, not a runtime check. +//! +//! The nonce is [`GCM_NONCE_LEN`] (12 bytes) and the tag 16, which is what RFC 8998 Sec 3.5.1 +//! fixes for `AEAD_SM4_GCM`: "The nonce length is 12 octets. The authentication tag length is 16 +//! octets." For a shorter tag, name the key's mode as `GCM<12>` and so on, within the `12..=16` +//! bytes `Gcm` accepts. The RFC's Appendix A.1 vector is pinned in `tests/rfc8998_tests.rs`. +//! +//! # Usage Examples +//! +//! The detached one-shots return the tag separately from the ciphertext: +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::GCM; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // The associated data is authenticated but not encrypted; the message is both. +//! let aad = b"header, sent in the clear"; +//! let message = b"a message of no particular length"; +//! +//! // The nonce is drawn from `rng` and returned. +//! let (nonce, ciphertext, tag) = key.encrypt_detached(&mut rng, aad, message).expect("encryption"); +//! assert_eq!(ciphertext.len(), message.len(), "GCM never expands the payload"); +//! +//! let recovered = key.decrypt_detached(&nonce, aad, &ciphertext, &tag).expect("decryption"); +//! assert_eq!(recovered, message); +//! +//! // Tampering with the ciphertext, the tag or the associated data fails the tag check. +//! let mut tampered = ciphertext.clone(); +//! tampered[0] ^= 1; +//! assert!(key.decrypt_detached(&nonce, aad, &tampered, &tag).is_err()); +//! assert!(key.decrypt_detached(&nonce, b"other header", &ciphertext, &tag).is_err()); +//! ``` +//! +//! Through the `_with_aad` one-shots the tag is appended to the ciphertext, and the streaming +//! methods take the data in pieces; `bouncycastle_aes::gcm` shows both, and they are the same +//! calls on this key. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from `bouncycastle_cipher::modes::gcm` apply, above all that a +//! nonce repeated under one key lets an attacker recover the hash subkey and forge tags, which +//! RFC 8998 Sec 3.5.1 restates for `AEAD_SM4_GCM`: "Using the same nonce for two different +//! messages encrypted with the same key destroys the security properties of GCM mode." + +use crate::hazmat::SM4Internal; +use bouncycastle_cipher::modes::Gcm; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::modes::GCM_NONCE_LEN; +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +// end of imports needed for docs + +/// SM4 in GCM with a 128-bit tag. +#[allow(non_camel_case_types)] +pub type SM4_GCM<'k, Dir> = Gcm<'k, SM4Internal, Dir, 16>; diff --git a/crypto/sm4/src/hazmat/mod.rs b/crypto/sm4/src/hazmat/mod.rs new file mode 100644 index 000000000..c358e8d65 --- /dev/null +++ b/crypto/sm4/src/hazmat/mod.rs @@ -0,0 +1,10 @@ +//! The raw SM4 permutation, [`SM4Internal`]; see [`bouncycastle_core::hazmat`] for what the path +//! means and the supported uses. +//! +//! [`SM4Internal`] transforms exactly one block and is the primitive under the modes in this +//! crate, not a cipher for data; use [`SM4Key`](crate::SM4Key) under [`SM4_CBC`](crate::SM4_CBC), +//! [`SM4_CFB`](crate::SM4_CFB), [`SM4_CFB8`](crate::SM4_CFB8) or [`SM4_CTR`](crate::SM4_CTR). + +pub(crate) mod sm4; + +pub use sm4::SM4Internal; diff --git a/crypto/sm4/src/hazmat/sm4.rs b/crypto/sm4/src/hazmat/sm4.rs new file mode 100644 index 000000000..c3cf651b0 --- /dev/null +++ b/crypto/sm4/src/hazmat/sm4.rs @@ -0,0 +1,404 @@ +//! The encryption and decryption algorithm (GB/T 32907-2016, as described in +//! draft-ribose-cfrg-sm4-10 Sec 6 and Sec 7), and the public engine type. + +use crate::sbox::tau; +use crate::schedule::{RoundKeys, expand}; +use crate::{BLOCK_LEN, KEY_LEN, LANES}; +use bouncycastle_cipher::modes::ModeNames; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::secret::Secret; + +/// One SM4 block. +pub type Block = [u8; BLOCK_LEN]; + +/// The SM4 keyed permutation, constant-time: the engine under [`SM4Key`](crate::SM4Key). +/// +/// The only state is the 32 round keys, held in a [`Secret`] so that they are zeroized on drop and +/// redacted from `Debug`. There is no direction flag and no initialisation state: decryption is +/// encryption with the round keys read backwards (Sec 7.2), so both directions work from the same +/// stored schedule, and a constructed value is always ready to use -- there is no `init()` or +/// `reset()`, unlike an engine that expands the key differently depending on the direction it is +/// initialised for. +#[derive(Clone)] +pub struct SM4Internal { + rk: Secret, +} + +// `Secret`'s equality inspects every byte of the schedule, so the time taken does not say where two +// differ. +impl PartialEq for SM4Internal { + fn eq(&self, other: &Self) -> bool { + self.rk == other.rk + } +} + +impl Eq for SM4Internal {} + +impl SM4Internal { + /// Checks a key is fit to use before it is expanded. + /// + /// The key must be tagged [`KeyType::SymmetricCipherKey`], must be exactly [`KEY_LEN`] bytes + /// of the buffer, and must carry a [`SecurityStrength`] of at least 128 bits -- which is what a + /// 16-byte key from a correctly-instantiated RNG or KDF will have. The checks exist to catch a + /// key that arrived from somewhere it should not have: a seed reused as a cipher key, or a + /// buffer holding material only derived at a lower strength. + /// + /// Returns the key bytes as a fixed-size array, so that the length check and the conversion + /// the schedule needs are one and the same operation. + fn validate(key: &KeyMaterial) -> Result<&[u8; KEY_LEN], SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "SM4 requires a key of type KeyType::SymmetricCipherKey.", + ) + .into()); + } + // `ref_to_bytes()` is the *populated* part of the buffer, `key_len()` bytes long. The + // conversion succeeds exactly when that is the whole 16 bytes, so it is the length check. + let bytes: &[u8; KEY_LEN] = + key.ref_to_bytes().try_into().map_err(|_| KeyMaterialError::InvalidLength)?; + if key.security_strength() < SecurityStrength::from_bytes(KEY_LEN) { + return Err(KeyMaterialError::SecurityStrength( + "The provided key has a lower security strength than the SM4 key length implies.", + ) + .into()); + } + Ok(bytes) + } + + /// Expands a 16-byte key into the 32 round keys (Sec 7.3). + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not [`KeyType::SymmetricCipherKey`]. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + pub(crate) fn new(key: &KeyMaterial) -> Result { + let bytes = Self::validate(key)?; + Ok(Self { rk: expand(bytes) }) + } + + /// Encrypts four independent blocks in place (Sec 7.1): the round keys in the order + /// `rk_0, ..., rk_31`. + /// + /// This is the natural unit of work. The S-box circuit substitutes 16 bytes per pass and a + /// round substitutes four bytes per block, so four blocks fill it exactly; fewer blocks cost + /// the same. Modes whose blocks are independent -- CTR, and the decryption direction of CBC + /// and CFB -- reach it as the `BlockPermutation` four-block batch; CBC encryption cannot, + /// since its blocks are serially dependent. + /// + /// Infallible: a constructed [`SM4Internal`] is always usable and every input length is fixed. + pub fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + rounds(blocks, |i| self.rk[i]); + } + + /// Decrypts four independent blocks in place (Sec 7.2): "an identical process as encryption, + /// with the only difference the order of the round key sequence", `rk_31, rk_30, ..., rk_0`. + /// See [`SM4Internal::encrypt_4blocks`]. + pub fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + rounds(blocks, |i| self.rk[31 - i]); + } + + /// Encrypts one block in place. + /// + /// The circuit always processes four lanes, so a single-block call puts the block in every + /// lane and discards three results: it does four blocks' worth of work. Use + /// [`SM4Internal::encrypt_4blocks`] where independent blocks are + /// available. + /// + /// Filling the spare lanes with copies costs exactly what zeros would, and buys a free + /// self-check: all four lanes must agree, which `debug_assert` verifies. It is not a security + /// property; the spare lanes are never returned either way. + pub(crate) fn encrypt_block(&self, block: &mut Block) { + let mut lanes = [*block; LANES]; + self.encrypt_4blocks(&mut lanes); + debug_assert!(lanes.iter().all(|b| *b == lanes[0]), "all lanes must agree"); + *block = lanes[0]; + } + + /// Decrypts one block in place. See [`BlockPermutation::encrypt_block`] for the four-lane + /// caveat. + pub(crate) fn decrypt_block(&self, block: &mut Block) { + let mut lanes = [*block; LANES]; + self.decrypt_4blocks(&mut lanes); + debug_assert!(lanes.iter().all(|b| *b == lanes[0]), "all lanes must agree"); + *block = lanes[0]; + } + + /// Encrypts two blocks in place, in lanes 0 and 1; the other two lanes carry copies of the + /// first and are discarded. Two blocks for the price of four, but twice as good as two + /// [`BlockPermutation::encrypt_block`] calls, which is why the trait method uses it. + pub(crate) fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut lanes = [blocks[0]; LANES]; + lanes[1] = blocks[1]; + self.encrypt_4blocks(&mut lanes); + *blocks = [lanes[0], lanes[1]]; + } + + /// Decrypts two blocks in place. See [`BlockPermutation::encrypt_2blocks`]. + pub(crate) fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut lanes = [blocks[0]; LANES]; + lanes[1] = blocks[1]; + self.decrypt_4blocks(&mut lanes); + *blocks = [lanes[0], lanes[1]]; + } +} + +/// `L(B) = B xor (B <<< 2) xor (B <<< 10) xor (B <<< 18) xor (B <<< 24)` (Sec 6.2.2). +#[inline(always)] +fn l(b: u32) -> u32 { + b ^ b.rotate_left(2) ^ b.rotate_left(10) ^ b.rotate_left(18) ^ b.rotate_left(24) +} + +/// The 32 rounds and the reverse transformation `R` (Sec 7.1) on four blocks at once, with +/// `rk(i)` supplying the round key for round `i` -- `rk_i` for encryption, `rk_{31-i}` for +/// decryption (Sec 7.2). +/// +/// Sec 7.1 (a): for `i = 0, 1, ..., 31`, `X_{i+4} = F(X_i, X_{i+1}, X_{i+2}, X_{i+3}, rk_i)` with +/// `F(X_0, X_1, X_2, X_3, rk) = X_0 xor T(X_1 xor X_2 xor X_3 xor rk)` (Sec 6.1) and +/// `T(.) = L(tau(.))` (Sec 6.2). Each round consumes the oldest of four live words and produces +/// one new one. Instead of sliding the window, the *roles* of the four slots rotate: in round `i` +/// the oldest word is in slot `i mod 4`, and after four rounds the slots are back in their +/// starting roles. The literal one-round-at-a-time form is in `tests::literal_rounds`, which pins +/// every intermediate `X_i` of Appendix A.1.1 and A.1.4 and agrees with this function. +/// +/// The four blocks are processed together only because `tau` is: the argument +/// `X_{i+1} xor X_{i+2} xor X_{i+3} xor rk_i` is formed per block, all four go through the +/// circuit in one pass, and `L` and the final XOR are again per block. The blocks never mix. +/// +/// Sec 7.1 (b): `(Y_0, Y_1, Y_2, Y_3) = R(X_32, X_33, X_34, X_35) = (X_35, X_34, X_33, X_32)`. +/// 32 is a multiple of 4, so after the loop slot `s` holds `X_{32+s}` and the output is the slots +/// in reverse order. +/// +/// Words are big-endian, as the worked examples require: Appendix A.1.2 shows the plaintext +/// `01 23 45 67 ...` recovered as `X_4 = 01234567`. +fn rounds(blocks: &mut [Block; LANES], rk: impl Fn(usize) -> u32) { + // x[b] = (X_0, X_1, X_2, X_3) of block b. + let mut x = [[0u32; 4]; LANES]; + for (xb, block) in x.iter_mut().zip(blocks.iter()) { + for (word, bytes) in xb.iter_mut().zip(block.as_chunks::<4>().0) { + *word = u32::from_be_bytes(*bytes); + } + } + + // (a) 32 rounds. + for i in 0..32 { + let (s0, s1, s2, s3) = (i % 4, (i + 1) % 4, (i + 2) % 4, (i + 3) % 4); + let k = rk(i); + + // The argument of T, one word per block. + let mut a = [0u32; LANES]; + for (ab, xb) in a.iter_mut().zip(x.iter()) { + *ab = xb[s1] ^ xb[s2] ^ xb[s3] ^ k; + } + // tau on all four words at once, then L and the XOR into X_i per block. + tau(&mut a); + for (ab, xb) in a.iter().zip(x.iter_mut()) { + xb[s0] ^= l(*ab); + } + } + + // (b) reverse transformation R. + for (xb, block) in x.iter().zip(blocks.iter_mut()) { + for (word, bytes) in xb.iter().rev().zip(block.as_chunks_mut::<4>().0) { + *bytes = word.to_be_bytes(); + } + } +} + +impl Algorithm for SM4Internal { + /// `"SM4"`. + const ALG_NAME: &'static str = "SM4"; + /// 128 bits: the one key length SM4 has (Sec 4). Sec 12 positions it as "an alternative to + /// AES-128". + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +// The `ElectronicCodeBook` / `BlockPermutation` impl pair is one-line delegations to the inherent +// methods above. The pair and four-block methods use the lanes: two blocks in two of the four lanes +// cost one circuit pass per round where two single-block calls would cost two, and four blocks +// are one full pass where two pair calls would be two. + +impl ElectronicCodeBook for SM4Internal { + fn new(key: &KeyMaterial) -> Result { + SM4Internal::new(key) + } +} + +impl BlockPermutation for SM4Internal { + fn encrypt_block(&self, block: &mut Block) { + SM4Internal::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + SM4Internal::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + SM4Internal::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + SM4Internal::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + SM4Internal::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + SM4Internal::decrypt_4blocks(self, blocks) + } +} + +// SM4 has published modes beyond the four this crate aliases -- draft-ribose-cfrg-sm4-10 Sec 8 +// defines ECB, and RFC 8998 Sec 3.5 defines AEAD_SM4_GCM and AEAD_SM4_CCM -- so every mode is +// named, although the keys hand out workers for the four aliased modes only. +impl ModeNames for SM4Internal { + const CBC_ALG_NAME: &'static str = "SM4_CBC"; + const CCM_ALG_NAME: &'static str = "SM4_CCM"; + const CFB_ALG_NAME: &'static str = "SM4_CFB"; + const CFB8_ALG_NAME: &'static str = "SM4_CFB8"; + const CMAC_ALG_NAME: &'static str = "SM4_CMAC"; + const CTR_ALG_NAME: &'static str = "SM4_CTR"; + const ECB_ALG_NAME: &'static str = "SM4_ECB"; + const GCM_ALG_NAME: &'static str = "SM4_GCM"; + const GMAC_ALG_NAME: &'static str = "SM4_GMAC"; +} + +impl core::fmt::Debug for SM4Internal { + /// Prints the algorithm name only. The round keys are secret and are never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(Self::ALG_NAME) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `T(.) = L(tau(.))` on one word (Sec 6.2), via the four-lane `tau` with the word in every + /// lane. + fn t(z: u32) -> u32 { + let mut words = [z; LANES]; + tau(&mut words); + assert!(words.iter().all(|&w| w == words[0]), "lanes must agree"); + l(words[0]) + } + + /// The round function `F(X_0, X_1, X_2, X_3, rk) = X_0 xor T(X_1 xor X_2 xor X_3 xor rk)` + /// (Sec 6.1), on one block. + fn f(x0: u32, x1: u32, x2: u32, x3: u32, rk: u32) -> u32 { + x0 ^ t(x1 ^ x2 ^ x3 ^ rk) + } + + /// Sec 7.1 (a) written literally, one round per step on one block, returning all of + /// `X_0 .. X_35`. + /// + /// This is the reference the slot-rotating, four-lane [`rounds`] is checked against, and it + /// is what lets the per-round `X_i` columns of Appendix A.1 be pinned rather than only the + /// final ciphertext. + fn literal_rounds(block: &Block, rk: &RoundKeys) -> [u32; 36] { + let mut x = [0u32; 36]; + for (i, w) in block.as_chunks::<4>().0.iter().enumerate() { + x[i] = u32::from_be_bytes(*w); + } + for i in 0..32 { + x[i + 4] = f(x[i], x[i + 1], x[i + 2], x[i + 3], rk[i]); + } + x + } + + fn check_example( + key: &[u8; 16], + plaintext: &Block, + expected_x: &[u32; 32], + ciphertext: &Block, + ) { + let sm4 = SM4Internal::new( + &KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(), + ) + .unwrap(); + + // Every intermediate round output X_4 .. X_35 against the appendix. + let x = literal_rounds(plaintext, &sm4.rk); + for (i, want) in expected_x.iter().enumerate() { + assert_eq!(x[i + 4], *want, "X_{}", i + 4); + } + + // R(X_32, X_33, X_34, X_35) = (X_35, X_34, X_33, X_32) is the ciphertext. + let mut from_literal = [0u8; 16]; + for (j, word) in [x[35], x[34], x[33], x[32]].iter().enumerate() { + from_literal[4 * j..4 * j + 4].copy_from_slice(&word.to_be_bytes()); + } + assert_eq!(&from_literal, ciphertext, "literal Sec 7.1 must give the ciphertext"); + + // ...and the slot-rotating four-lane form agrees with the literal one, with the block in + // any lane and unrelated blocks in the others. + let lanes: [Block; LANES] = core::array::from_fn(|i| [i as u8 * 17 + 1; 16]); + let mut alone = lanes; + for b in alone.iter_mut() { + sm4.encrypt_block(b); + } + for lane in 0..LANES { + let mut mixed = lanes; + mixed[lane] = *plaintext; + sm4.encrypt_4blocks(&mut mixed); + assert_eq!(mixed[lane], from_literal, "lane {lane} must match the literal form"); + for other in (0..LANES).filter(|&o| o != lane) { + assert_eq!(mixed[other], alone[other], "lane {other} must be undisturbed"); + } + } + } + + /// Appendix A.1.1 (GB/T 32907-2016 Example 1): the per-round X_i column. + #[test] + fn test_round_outputs_match_appendix_a_1_1() { + let key = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, + 0x32, 0x10, + ]; + let plaintext = key; + let expected_x: [u32; 32] = [ + 0x27FAD345, 0xA18B4CB2, 0x11C1E22A, 0xCC13E2EE, 0xF87C5BD5, 0x33220757, 0x77F4C297, + 0x7A96F2EB, 0x27DAC07F, 0x42DD0F19, 0xB8A5DA02, 0x907127FA, 0x8B952B83, 0xD42B7C59, + 0x2FFC5831, 0xF69E6888, 0xAF2432C4, 0xED1EC85E, 0x55A3BA22, 0x124B18AA, 0x6AE7725F, + 0xF4CBA1F9, 0x1DCDFA10, 0x2FF60603, 0xEFF24FDC, 0x6FE46B75, 0x893450AD, 0x7B938F4C, + 0x536E4246, 0x86B3E94F, 0xD206965E, 0x681EDF34, + ]; + let ciphertext = [ + 0x68, 0x1E, 0xDF, 0x34, 0xD2, 0x06, 0x96, 0x5E, 0x86, 0xB3, 0xE9, 0x4F, 0x53, 0x6E, + 0x42, 0x46, + ]; + check_example(&key, &plaintext, &expected_x, &ciphertext); + } + + /// Appendix A.1.4 (Example 4): the per-round X_i column. + #[test] + fn test_round_outputs_match_appendix_a_1_4() { + let key = [ + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, + ]; + let plaintext = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, + ]; + let expected_x: [u32; 32] = [ + 0xF7EAEB6A, 0xB4967C0F, 0x5B9B2419, 0xF46BECBA, 0xA8013E25, 0xB38E2ABE, 0x3E7C99A1, + 0x6DD5F47F, 0xB286430C, 0xAB997DE3, 0x80F8F21F, 0x4EF7052E, 0x4462FFAF, 0x14DFD5EA, + 0x6D33EFED, 0x3A4F8B3C, 0x1A435088, 0x4E64B153, 0x0415CEDA, 0xADD88955, 0x73964EF1, + 0xB0085092, 0x554A1293, 0x4BC6D6A8, 0x7BB650E1, 0xDDFB8A61, 0x5C4DFD78, 0xFD9066FD, + 0x55ADB594, 0xAC1B3EA9, 0x13F01ADE, 0xF766678F, + ]; + let ciphertext = [ + 0xF7, 0x66, 0x67, 0x8F, 0x13, 0xF0, 0x1A, 0xDE, 0xAC, 0x1B, 0x3E, 0xA9, 0x55, 0xAD, + 0xB5, 0x94, + ]; + check_example(&key, &plaintext, &expected_x, &ciphertext); + } + + #[test] + fn test_l_is_the_documented_rotation_sum() { + // L(B) = B xor (B <<< 2) xor (B <<< 10) xor (B <<< 18) xor (B <<< 24) + assert_eq!(l(1), 1 | (1 << 2) | (1 << 10) | (1 << 18) | (1 << 24)); + assert_eq!(l(0), 0); + } +} diff --git a/crypto/sm4/src/key.rs b/crypto/sm4/src/key.rs new file mode 100644 index 000000000..a29827aff --- /dev/null +++ b/crypto/sm4/src/key.rs @@ -0,0 +1,244 @@ +//! SM4 key objects: a key bound to one mode, validated and expanded once, from which the mode's +//! encryptors and decryptors are made. +//! +//! [`SM4Key`] is one type per mode `M`, as `AES128Key` is in `bouncycastle-aes`: the key +//! holds the mode's per-key state, the workers it hands out borrow it, and which role pair a key +//! carries is decided by its mode: the symmetric pair for the stream modes and the padded block +//! modes (`CFB`, `CFB8`, `CTR`, `CBC`, `ECB`), the block pair for `CBC`, +//! and the AEAD pair for `GCM`. The five modes this crate aliases as `SM4_*` are among them; a +//! key under any other mode has no role here. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::>::from_key_material(&material).expect("a valid SM4 key"); +//! +//! let (iv, ciphertext) = key.encrypt(&mut rng, b"a message of no particular length").unwrap(); +//! assert_eq!(key.decrypt(&iv, &ciphertext).unwrap(), b"a message of no particular length"); +//! ``` +//! +//! # Memory Usage +//! +//! A key is its mode's per-key state: the 128-byte round-key schedule, which the four +//! unauthenticated aliased modes keep alone, plus the 16-byte hash subkey under `GCM`, 144 bytes. +//! The cost is paid per key, not per message. + +use crate::hazmat::SM4Internal; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ + AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, Mode, ModeKey, + SymmetricDecryptMode, SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, + SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::RNG; + +/// An SM4 key for the mode `M`: 16 bytes of material, expanded to the 32 round keys. +/// +/// The only state is the mode's per-key state, which holds the expanded schedule in a `Secret` +/// and so is zeroized on drop; the key has no `Debug` and no way to read the material back. +pub struct SM4Key> { + state: M::KeyState, +} + +// Written out rather than derived so that `M`, a marker type, need not be `Clone`. +impl> Clone for SM4Key { + fn clone(&self) -> Self { + Self { state: self.state.clone() } + } +} + +// Likewise for equality: two keys are equal when their mode states are, which every state over +// the engine decides in constant time, so the comparison follows the material without exposing it. +impl> PartialEq for SM4Key +where + M::KeyState: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl> Eq for SM4Key where M::KeyState: Eq {} + +impl> FromKeyMaterial<{ crate::KEY_LEN }> for SM4Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Validates and expands a 16-byte key: the key type the mode asks for, then the + /// permutation's own checks. Every mode with a role here asks for a `SymmetricCipherKey`, so + /// under a MAC mode, which asks for a `MACKey`, no material passes both. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`], or not a + /// `SymmetricCipherKey`. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + fn from_key_material( + key: &KeyMaterial<{ crate::KEY_LEN }>, + ) -> Result { + if key.key_type() != M::KEY_TYPE { + return Err(KeyMaterialError::InvalidKeyType( + "The key is not of the KeyType this mode requires.", + ) + .into()); + } + Ok(Self { state: M::key_state(SM4Internal::new(key)?) }) + } +} + +impl> ModeKey for SM4Key { + fn state(&self) -> &M::KeyState { + &self.state + } +} + +// The role impls forward to the mode traits `M` implements over this permutation: the symmetric +// pair, the block pair and the AEAD pair, which between them cover the five modes this crate +// aliases. A key under any other mode has no role here. + +impl + SymmetricEncryptionKey for SM4Key +where + M: SymmetricEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + SymmetricDecryptionKey for SM4Key +where + M: SymmetricDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl + BlockEncryptionKey for SM4Key +where + M: BlockEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + BlockDecryptionKey for SM4Key +where + M: BlockDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl + AeadEncryptionKey for SM4Key +where + M: AeadEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } + + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { + M::encrypt_init_nonce(&self.state, nonce) + } + + // Forwarded rather than left to the trait's provided body so that a mode can override it, + // as the AES key does. + fn encrypt_detached_in_place( + &self, + rng: &mut dyn RNG, + aad: &[u8], + buf: &mut [u8], + ) -> Result<([u8; NONCE_LEN], [u8; TAG_LEN]), SymmetricCipherError> { + M::encrypt_detached_in_place(&self.state, rng, aad, buf) + } + + fn encrypt_detached_in_place_nonce( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + ) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + M::encrypt_detached_in_place_nonce(&self.state, nonce, aad, buf) + } +} + +impl + AeadDecryptionKey for SM4Key +where + M: AeadDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, nonce) + } + + fn decrypt_detached_in_place( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + tag: &[u8; TAG_LEN], + ) -> Result<(), SymmetricCipherError> { + M::decrypt_detached_in_place(&self.state, nonce, aad, buf, tag) + } +} diff --git a/crypto/sm4/src/lib.rs b/crypto/sm4/src/lib.rs new file mode 100644 index 000000000..40353e081 --- /dev/null +++ b/crypto/sm4/src/lib.rs @@ -0,0 +1,321 @@ +//! A constant-time, table-free, low-memory SM4 block cipher engine (GB/T 32907-2016), ported from +//! Bouncy Castle Java's `SM4Engine`. +//! +//! This crate provides SM4 -- a 128-bit block cipher with a single 128-bit key length, +//! standardised by the State Cryptography Administration of China as GB/T 32907-2016 and described +//! in English in the CFRG document `draft-ribose-cfrg-sm4-10`, which is the specification every +//! comment in this crate cites -- under the library's key objects: [`SM4Key`], a key bound to +//! one mode `M`, validated and expanded once, handing out that mode's encryptors and decryptors. +//! The engine underneath, [`SM4Internal`], evaluates the S-box as a Boolean circuit over +//! bit-planes rather than looking it up in a table, so it is constant-time and carries no table at +//! all; the planes are 16 bits wide so that the working set stays small, which is what "lowmemory" +//! means here. See [Design](#design). +//! +//! The engine is a *permutation*, not a cipher you can encrypt data with, which is why it lives +//! under [`hazmat`]. See [Security Considerations](#-security-considerations-). +//! +//! # Usage Examples +//! +//! ## Modes of operation +//! +//! A key is built once, under a mode from `bouncycastle_cipher::modes`, and used for as many +//! messages as needed: `CBC` for CBC (Sec 8.4; NIST SP 800-38A Sec 6.2) with PKCS#7 +//! padding, `CFB` for SM4-CFB-128 (Sec 8.5.1), `CFB8` for SM4-CFB-8, the `s = 8` segment size, +//! which is a different and non-interoperable mode costing one SM4 call per byte, `CTR` for +//! CTR (Sec 8.7) with a 12-byte nonce and a 4-byte counter, and `GCM` for the authenticated mode +//! RFC 8998 Sec 3.5.1 defines as `AEAD_SM4_GCM`, with a 12-byte nonce and a 16-byte tag. The +//! aliases [`SM4_CBC`], [`SM4_CFB`], [`SM4_CFB8`], [`SM4_CTR`] and [`SM4_GCM`] name the workers +//! the key hands out. +//! +//! CBC is a block cipher, so it is defined only on whole blocks and the key's mode carries a +//! padding scheme to bridge the difference; the CFB modes and CTR are stream ciphers and take any +//! length with no padding at all. See the `bouncycastle_cipher::modes` crate docs for the +//! comparison, and [`SM4_CBC`] for why the scheme is named in the type. +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::>::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. +//! let plaintext = [0x5Au8; 50]; +//! +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, &plaintext).expect("encryption"); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! The stream modes take any length and return the initialisation data the same way: +//! +//! ``` +//! use bouncycastle_sm4::SM4Key; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = SM4Key::::from_key_material(&material).expect("a valid SM4 key"); +//! +//! // 50 bytes, and the ciphertext is 50 bytes: no padding anywhere. +//! let plaintext = [0x5Au8; 50]; +//! let mut data = plaintext; +//! let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); +//! assert_ne!(data, plaintext); +//! +//! key.decrypt_inplace(&nonce, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! For the block-aligned API -- whole blocks in place, through the block role pair -- make the +//! key under `CBC`; the padded alias wraps the same worker. +//! +//! ## The raw permutation +//! +//! The engine transforms one block, and `ElectronicCodeBook::new` on it is the one-shot: +//! +//! ``` +//! use bouncycastle_sm4::hazmat::SM4Internal; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! // GB/T 32907-2016 Example 1 (draft-ribose-cfrg-sm4-10 Appendix A.1.1). +//! let key = KeyMaterial::<16>::from_bytes_as_type( +//! &[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, +//! 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 16-byte symmetric cipher key"); +//! +//! let sm4 = SM4Internal::new(&key).expect("a valid SM4 key"); +//! +//! let mut block = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, +//! 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10]; +//! sm4.encrypt_block(&mut block); +//! assert_eq!(block, [0x68, 0x1E, 0xDF, 0x34, 0xD2, 0x06, 0x96, 0x5E, +//! 0x86, 0xB3, 0xE9, 0x4F, 0x53, 0x6E, 0x42, 0x46]); +//! +//! // The same value decrypts, from the same round keys -- there is no separate decryptor. +//! sm4.decrypt_block(&mut block); +//! assert_eq!(block, [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, +//! 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10]); +//! ``` +//! +//! ## Four blocks at a time +//! +//! The bit-sliced S-box substitutes 16 bytes per pass, and a round substitutes four bytes per +//! block, so four independent blocks cost the same as one. Where a caller has four, +//! [`SM4Internal::encrypt_4blocks`] is four times the throughput of four `encrypt_block` calls, +//! and it is also the four-block batch `BlockPermutation::encrypt_4blocks` offers to modes: +//! +//! ``` +//! use bouncycastle_sm4::hazmat::SM4Internal; +//! use bouncycastle_sm4::LANES; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let sm4 = SM4Internal::new(&key).expect("a valid SM4 key"); +//! +//! let mut blocks: [[u8; 16]; LANES] = core::array::from_fn(|i| [i as u8; 16]); +//! let original = blocks; +//! sm4.encrypt_4blocks(&mut blocks); +//! sm4.decrypt_4blocks(&mut blocks); +//! assert_eq!(blocks, original); +//! ``` +//! +//! # Design +//! +//! SM4 is a 32-round unbalanced Feistel network over four 32-bit words (Sec 4). Each round +//! replaces the oldest word with `X_0 xor T(X_1 xor X_2 xor X_3 xor rk_i)` (Sec 6.1), where `T` +//! is four parallel S-box lookups followed by a fixed linear map of five rotations (Sec 6.2). +//! Decryption is the same 32 rounds with the round keys in reverse order (Sec 7.2), and the key +//! schedule (Sec 7.3) is the same recurrence again with a different linear map and a constant +//! added each round. +//! +//! ## Why not a lookup table +//! +//! Sec 6.2.3 presents the S-box as a table (Figure 1), and a straightforward implementation +//! stores it as one. A table indexed by a byte of the state is indexed by secret data, so on any +//! CPU with a data cache the memory access pattern, and hence the timing, depends on the key and +//! the data. That is the classic cache-timing attack on table-driven block ciphers, and it is not +//! fixable while the lookup remains -- in the cipher or in the key schedule, whose `T'` also goes +//! through the S-box. +//! +//! ## The S-box as a circuit +//! +//! This crate has no table outside its tests. The S-box has the same algebraic shape as the AES +//! one -- an affine map, inversion in GF(2^8) (modulo `x^8 + x^7 + x^6 + x^5 + x^4 + x^2 + 1` +//! rather than the AES polynomial), and another affine map -- a decomposition found here by +//! exhaustive search against the Figure 1 table, not taken from recall. A field isomorphism into +//! the AES representation then lets the 62-gate non-linear section of the Boyar-Peralta AES +//! circuit, copied verbatim from `bouncycastle-aes`, do the inversion, with new affine +//! top and bottom layers derived by linear algebra and pinned by an exhaustive 256-input test. +//! The whole S-box is 127 gates: 32 AND, 82 XOR, 12 XNOR, 1 NOT. The `sbox` module docs give the +//! full derivation. +//! +//! A round substitutes only four bytes per block, and the circuit's eight `u16` planes hold 16 +//! byte positions, so the engine works on **four blocks at once**: each round forms the argument +//! of `T` for every block, splits the four words into eight 16-bit halves, transposes them into +//! planes, runs the circuit once, +//! transposes back, and finishes the round per block. The blocks never mix. Everything else in +//! the round -- the XORs, the five rotations of `L` -- is already constant-time on words, so the +//! rest of the engine is a straightforward word-oriented implementation, with the roles of the +//! four state words rotating each round rather than the words themselves moving. +//! +//! Beyond the S-box, this port stores one schedule that serves both directions (a direction-aware +//! engine typically expands the key in reverse when initialised for decryption); the block methods +//! are infallible (the run-time buffer and initialisation checks are compile-time facts here); and +//! the constructors require a key tagged as a symmetric cipher key of at least 128-bit strength, as +//! every cipher in this workspace does. +//! +//! ## Why "lowmemory" +//! +//! Two things. First, no table: a table-driven engine typically carries a 256-byte S-box indexed +//! by secret data; here the S-box is code. Second, the working set. Eight `u32` planes would take +//! eight blocks per pass and double the throughput, but every per-call buffer -- the block state +//! and the planes -- would double with them. Four lanes over `u16` planes keep the whole per-call +//! working state near 100 bytes. +//! +//! # Memory Usage +//! +//! No heap allocation and no lookup tables. The persistent state is the 32 round keys of Sec 7.3, +//! held once in the key; a worker adds the mode's own state and a reference to the key: +//! +//! | Type | Key | Rounds | Schedule (persistent) | Tables | +//! |---|---|---|---|---| +//! | [`SM4Internal`] | 16 B | 32 | 128 B | 0 B | +//! +//! Per-call stack usage is the four-block working state -- four words per block, 64 bytes -- +//! plus the four `T` arguments (16 bytes), the eight `u16` planes of the S-box argument (16 +//! bytes) and the circuit's spills. Every entry point runs the same four-lane circuit, so the +//! one-, two- and four-block calls cost the same working state. Measured as the deepest frame +//! chain below each entry point's `measure` closure in +//! `mem_usage_benches/src/bench_sm4_mem_usage.rs`: the closure, then the out-of-line +//! `encrypt_4blocks` (or `new`), `tau` and `ortho` frames under it, from the compiler's +//! frame-layout remarks and the disassembly of the release build (x86-64, rustc 1.97 nightly, +//! return addresses included): +//! +//! | Entry point | Stack | +//! |---|---| +//! | `new` (key expansion) | 704 B | +//! | `encrypt_block` / `decrypt_block` | 416 B | +//! | `encrypt_2blocks` / `decrypt_2blocks` | 432 B | +//! | `encrypt_4blocks` / `decrypt_4blocks` | 416 B | +//! +//! For comparison, a table-driven engine carries a 256-byte table on top of the same schedule. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## A block permutation is not a cipher +//! +//! [`SM4Internal`] transforms exactly 16 bytes. Using it directly on data means ECB, which is not +//! confidential: identical plaintext blocks produce identical ciphertext blocks, so structure in +//! the plaintext survives encryption (the specification's own Sec 12 says SM4-ECB "SHOULD NOT be +//! used in most cases"). **Do not do it.** Use a mode of operation, and prefer an authenticated +//! one so that ciphertext tampering is detected. That is why [`SM4Internal`] lives under +//! [`hazmat`]; [`bouncycastle_core::hazmat`] lists the supported uses. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, in +//! the cipher *or* in the key schedule: `T'` goes through the same circuit as `T`. The only +//! branches are the round loop and the lane loops, which count over public constants. +//! +//! Caveats worth stating plainly: +//! +//! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the +//! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual +//! ways of forcing the issue, but the property is not contractual. +//! * The four-block working state is not scrubbed after a call. Only the round keys are wrapped +//! in `Secret`, and so only they are guaranteed to be zeroized on drop. +//! * Constant-time execution says nothing about power or electromagnetic side channels, which +//! Sec 12 of the specification specifically raises for SM4 hardware. +//! +//! ## Regulatory note +//! +//! SM4 is a Chinese national standard. Sec 12 of the specification notes that products using +//! cryptography are regulated by the State Cryptography Administration and must be approved before +//! sale or use in China. Nothing in this crate addresses that. +//! +//! # Provenance +//! +//! * **Source engine.** The `FK` and `CK` tables, round structure and key schedule this crate +//! reproduces, and the S-box table the circuit is verified against, come from the reference +//! engine named at the top of this page. A direct transcription of it lives in the tests +//! (`tests/common/mod.rs` and `tests/bc_java_tests.rs`), and this engine is checked against it on +//! thousands of inputs. +//! * **Normative reference: GB/T 32907-2016**, read via `draft-ribose-cfrg-sm4-10`, "The SM4 +//! Blockcipher Algorithm And Its Modes Of Operations" (Tse, Wong, Saarinen; CFRG, April 2018). +//! Every function cites its section. The S-box and `CK` tables were extracted mechanically from +//! the text of the draft; `CK` is additionally re-derived from its defining formula in a test. +//! * **The non-linear section of the S-box circuit** is from the 113-gate straight-line program +//! `SLP_AES_113.txt` in Peralta's circuit collection, described in J. Boyar and R. Peralta, "A +//! new combinational logic minimization technique with applications to cryptology", +//! , as transcribed in `bouncycastle-aes`. The +//! algebraic structure of the SM4 S-box that makes the reuse possible is analysed in Liu, Ji, +//! Hu, Ding and Lv, "Analysis of the SMS4 Block Cipher" (ACISP 2007); here it was re-derived by +//! search and verified exhaustively. +//! * **The bit-plane transpose** is translated from BearSSL's `aes_ct` by Thomas Pornin (MIT +//! licence), as in the AES crate. +//! * Verified against every value in the draft's Appendix A.1 -- Examples 1 through 6, including +//! all 32 round keys and all 32 per-round outputs of Examples 1 and 4, and the two 1,000,000-fold +//! iterated ciphertexts -- the SM4-ECB, SM4-CBC and SM4-CFB vectors of Appendix A.2.1, A.2.2 and +//! A.2.4, the SM4-CTR vectors of Appendix A.2.5 (through a reference built from the Sec 8.7.1 +//! equations, which the `CTR` key is then held to -- see `tests/stream_mode_tests.rs` for +//! why the published counter blocks are not ones it produces), plus GB/T 32907-2016's own +//! two examples as republished at . RFC 8998 Appendix +//! A.1 and Project Wycheproof's `sm4_gcm_test.json` run through the `GCM` key; Appendix A.2 +//! and `sm4_ccm_test.json` through the `CCM` mode traits over the engine. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod bitslice; +mod cbc; +mod cfb; +mod cfb8; +mod ctr; +mod gcm; +pub mod hazmat; +mod key; +mod sbox; +mod schedule; + +/// The SM4 block length in bytes: 16 (Sec 4, "block size of 128 bits"). +pub const BLOCK_LEN: usize = 16; + +/// The SM4 key length in bytes: 16 (Sec 4, "key length of 128 bits"). There is only one. +pub const KEY_LEN: usize = 16; + +/// The number of blocks the bit-sliced S-box substitutes at once. See +/// [`SM4Internal::encrypt_4blocks`]. +pub const LANES: usize = 4; + +pub use cbc::SM4_CBC; +pub use cfb::SM4_CFB; +pub use cfb8::SM4_CFB8; +pub use ctr::{CTR_NONCE_LEN, SM4_CTR}; +pub use gcm::SM4_GCM; +pub use key::SM4Key; + +// Imports needed for docs +#[allow(unused_imports)] +use hazmat::SM4Internal; +// end of imports needed for docs diff --git a/crypto/sm4/src/sbox.rs b/crypto/sm4/src/sbox.rs new file mode 100644 index 000000000..db25edbfd --- /dev/null +++ b/crypto/sm4/src/sbox.rs @@ -0,0 +1,395 @@ +//! The S-box `S` and the nonlinear transformation `tau` as a Boolean circuit +//! (GB/T 32907-2016, as described in draft-ribose-cfrg-sm4-10 Sec 6.2.1 and Sec 6.2.3). +//! +//! # Why a circuit and not a table +//! +//! Sec 6.2.3 presents the S-box as a 256-entry lookup table (Figure 1), and a straightforward +//! implementation stores it as one. A table indexed by a byte of the state is indexed by *secret +//! data*, and on any CPU with a data cache the access pattern -- hence the timing -- depends on +//! that secret. That is the standard cache-timing side channel of every table-driven block cipher, +//! and it cannot be closed while keeping the lookup. +//! +//! So this module has no table (outside its tests). It computes the same function as Figure 1 +//! with AND, XOR, XNOR and NOT gates applied to the bit-planes of [`crate::bitslice`]. Every +//! operation is a straight-line word operation on public *positions*, so there is no +//! secret-dependent memory access and no secret-dependent branch. This is the only place in the +//! crate where secret data meets non-linear logic; everything else is XOR and rotate. +//! +//! Because the planes hold the four bytes of one word from each of four blocks -- as two 16-bit +//! halves each -- one pass of the circuit is `tau` for four blocks at once. +//! +//! # What the circuit computes, and where it came from +//! +//! The SM4 S-box has the same algebraic shape as the AES one: an affine map, inversion in +//! GF(2^8), and another affine map. Specifically, with `A` the circulant matrix whose rows are +//! `A7 4F 9E 3D 7A F4 E9 D3` (row `i` is the mask of input bits XORed into output bit `i`, bit 0 +//! least significant) and `C = 0xD3`, +//! +//! ```text +//! S(x) = A * inv(A * x xor C) xor C, inv in GF(2^8) mod x^8 + x^7 + x^6 + x^5 + x^4 + x^2 + 1 +//! ``` +//! +//! This decomposition was **found by search, not recalled**: every circulant affine map, every +//! constant and every irreducible degree-8 polynomial was tried against the Figure 1 table +//! extracted from the text of the draft, and exactly one decomposition (plus its bit-reversed +//! mirror) reproduces all 256 entries. It agrees with the analysis of Liu, Ji, Hu, Ding and Lv, +//! "Analysis of the SMS4 Block Cipher" (ACISP 2007). +//! +//! Because both S-boxes are inversion between affine maps, a field isomorphism `phi` from the +//! SM4 representation to the AES one (`x^8 + x^4 + x^3 + x + 1`) lets the AES inversion circuit +//! do SM4's work: `inv_sm4(w) = phi^-1(inv_aes(phi(w)))`. The circuit below is therefore +//! +//! * a **top affine layer**, generated: `phi(A * x xor C)` composed with the Boyar-Peralta input +//! basis change, as one affine map from the eight input planes to the 22 signals the +//! non-linear section consumes; +//! * the **non-linear section** of the 113-gate Boyar-Peralta AES circuit, **copied verbatim** +//! from `bouncycastle-aes` (`src/sbox.rs`, itself a transcription of Peralta's +//! `SLP_AES_113.txt`): 32 AND and 30 XOR gates computing the inverse in a tower-field +//! representation; +//! * a **bottom affine layer**, generated: the tower field back to the AES polynomial basis, +//! `phi^-1`, then `A * . xor C`, as one affine map from the 18 non-linear outputs to the eight +//! output planes. +//! +//! The two affine layers were fitted by linear algebra over GF(2) (the bottom one by solving for +//! the linear map from the non-linear section's outputs to `inv_aes`, on all 256 inputs) and +//! reduced with a greedy common-subexpression pass; `phi` was chosen among the eight roots of the +//! SM4 polynomial in the AES field to minimise the gate count. The result is 127 gates: 32 AND, +//! 82 XOR, 12 XNOR, 1 NOT. The generator verified the assembled circuit against the table before +//! any of it was written here, and `test_sbox_matches_figure_1` re-verifies all 256 inputs in +//! Rust. The gate list is not meaningful line by line and should not be "tidied". +//! +//! # Bit numbering +//! +//! Plane `k` holds bit `k` of every byte (bit 0 least significant), for inputs and outputs alike. +//! The Boyar-Peralta section keeps its own signal names (`y*`, `t*`, `z*`) so it can be diffed +//! against the AES crate; its inputs are bound from the generated top layer, so its `U0`-is-MSB +//! convention never appears here. + +use crate::LANES; +use crate::bitslice::{Planes, ortho}; + +/// `S` applied to every byte position of the eight words in `q` (Sec 6.2.3, Figure 1), on +/// bit-planes: `q[k]` holds bit `k` of each byte on entry and on exit. +/// +/// See the module docs for what the three sections are and where they come from. +pub(crate) fn sbox(q: &mut Planes) { + // Inputs: plane k holds bit k of every byte. + let x0 = q[0]; + let x1 = q[1]; + let x2 = q[2]; + let x3 = q[3]; + let x4 = q[4]; + let x5 = q[5]; + let x6 = q[6]; + let x7 = q[7]; + + // Top affine layer (32 XOR/XNOR gates, generated): the SM4 input affine map, the field + // isomorphism into the AES representation, and the Boyar-Peralta input basis change, folded + // into one linear map over x0..x7. NOTs carry the affine constants. + let a1 = x2 ^ x4; + let a2 = x0 ^ x3; + let a3 = x1 ^ x7; + let a4 = a1 ^ x5; + let a5 = a2 ^ x1; + let a6 = a2 ^ a3; + let a7 = x2 ^ x6; + let a8 = a1 ^ x7; + let a9 = a4 ^ x6; + let a10 = a6 ^ x4; + let a11 = a5 ^ x5; + let a12 = a3 ^ x3; + let a13 = a9 ^ x7; + let a14 = a11 ^ a7; + let a15 = x3 ^ x5; + let a16 = !(a15 ^ x7); + let a17 = a2 ^ a8; + let a18 = a17 ^ x6; + let a19 = a3 ^ a9; + let a20 = a7 ^ x1; + let a21 = !(a3 ^ a4); + let a22 = !(x2 ^ x7); + let a23 = !(a4 ^ x3); + let a24 = !(a1 ^ a5); + let a25 = !(a8 ^ x0); + let a26 = a10 ^ x5; + let a27 = a26 ^ x6; + let a28 = a2 ^ a4; + let a29 = a4 ^ a5; + let a30 = a3 ^ x0; + let a31 = a30 ^ x4; + let a32 = !(a31 ^ x6); + let u7 = a12; + let y14 = a13; + let y13 = x1; + let y9 = a10; + let y8 = a14; + let y1 = a16; + let y4 = a18; + let y12 = a19; + let y2 = a20; + let y5 = a7; + let y3 = a11; + let y15 = a21; + let y20 = a22; + let y6 = a23; + let y10 = a8; + let y11 = a24; + let y7 = a25; + let y17 = !a6; + let y19 = a27; + let y16 = a28; + let y21 = a29; + let y18 = a32; + + // Non-linear section (62 gates): GF(2^8) inversion, copied verbatim from the Boyar-Peralta + // AES circuit in `bouncycastle-aes` (`src/sbox.rs`). It consumes the signals bound + // above and produces the tower-field coordinates of the inverse. + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the only one of the circuit's 82 XOR gates with that + // property -- every other `^ -> |` mutant is killed by `test_sbox_matches_figure_1` -- and it + // is the same gate the AES crate documents, since this section is copied from there. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom affine layer (32 XOR/XNOR gates, generated): tower field back to the AES polynomial + // basis, the isomorphism back to the SM4 field, and the SM4 output affine map, folded into + // one affine map over the inverse's coordinates; XNORs and NOTs carry the 0xD3 constant. + let b1 = z0 ^ z10; + let b2 = z15 ^ z9; + let b3 = b1 ^ z1; + let b4 = z13 ^ z14; + let b5 = b4 ^ z6; + let b6 = b2 ^ b3; + let b7 = z5 ^ z8; + let b8 = b5 ^ z4; + let b9 = b6 ^ b7; + let b10 = b9 ^ z16; + let b11 = z3 ^ z7; + let b12 = b8 ^ z11; + let b13 = b2 ^ z17; + let b14 = !(b10 ^ b11); + let b15 = b12 ^ b3; + let b16 = !(b15 ^ b7); + let b17 = b11 ^ b13; + let b18 = b17 ^ b8; + let b19 = b18 ^ z10; + let b20 = b13 ^ z11; + let b21 = b10 ^ z4; + let b22 = !(b21 ^ z6); + let b23 = b5 ^ b6; + let b24 = b23 ^ z17; + let b25 = b24 ^ z7; + let b26 = b1 ^ b12; + let b27 = b26 ^ z2; + let b28 = b27 ^ z5; + let b29 = !(b28 ^ z7); + let b30 = z12 ^ z13; + let b31 = b30 ^ z15; + let b32 = !(b31 ^ z16); + + // Outputs: plane j receives bit j of S(x). + q[0] = b14; + q[1] = b16; + q[2] = b19; + q[3] = b20; + q[4] = b22; + q[5] = b25; + q[6] = b29; + q[7] = b32; +} + +/// `tau(A) = (S(a_0), S(a_1), S(a_2), S(a_3))` (Sec 6.2.1), on one word from each of four +/// blocks at once. +/// +/// Each word is split into its two 16-bit halves -- the high halves in plane words `0 .. 4`, the +/// low halves in `4 .. 8` -- the eight half-words are transposed into bit-planes, every byte +/// position is substituted by one pass of [`sbox`], the planes are transposed back and the halves +/// rejoined. The byte order within a word is irrelevant to `tau`, which substitutes each byte +/// independently; [`ortho`] being its own inverse is what returns each substituted byte to its +/// original position. +pub(crate) fn tau(words: &mut [u32; LANES]) { + let mut q: Planes = [0; 8]; + for b in 0..LANES { + q[b] = (words[b] >> 16) as u16; + q[LANES + b] = words[b] as u16; + } + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + for b in 0..LANES { + // The two halves occupy disjoint bit ranges, so `|` and `^` agree here -- a surviving + // `cargo mutants` equivalence, not a gap. + words[b] = ((q[b] as u32) << 16) | q[LANES + b] as u32; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Figure 1, "SM4 S-box Values", row-major: `SBOX[16 * row + column]`. Extracted mechanically + /// from the text of the draft; byte-for-byte the `Sbox` array in BC Java's `SM4Engine`. Test + /// data only -- the engine never reads it. + const SBOX: [u8; 256] = [ + 0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, + 0x05, 0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, + 0x06, 0x99, 0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, + 0xcf, 0xac, 0x62, 0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, + 0x75, 0x8f, 0x3f, 0xa6, 0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, + 0x19, 0xe6, 0x85, 0x4f, 0xa8, 0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, + 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35, 0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, + 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87, 0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, + 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e, 0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, + 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1, 0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, + 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3, 0x1d, 0xf6, 0xe2, 0x2e, 0x82, + 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f, 0xd5, 0xdb, 0x37, 0x45, + 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51, 0x8d, 0x1b, 0xaf, + 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8, 0x0a, 0xc1, + 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0, 0x89, + 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84, + 0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, + 0x48, + ]; + + /// The table-driven `tau` of BC Java, on one word. Test reference only. + fn tau_table(a: u32) -> u32 { + let [a0, a1, a2, a3] = a.to_be_bytes(); + u32::from_be_bytes([ + SBOX[a0 as usize], SBOX[a1 as usize], SBOX[a2 as usize], SBOX[a3 as usize], + ]) + } + + #[test] + fn test_table_matches_the_worked_example() { + // Sec 6.2.3: "input 'EF' will produce an output read from the S-box table row E and + // column F, giving the result S(EF) = 84." Pins the row-major flattening of the test data. + assert_eq!(SBOX[0xEF], 0x84); + assert_eq!(SBOX[0x00], 0xD6); + assert_eq!(SBOX[0x0F], 0x05); + assert_eq!(SBOX[0xF0], 0x18); + assert_eq!(SBOX[0xFF], 0x48); + } + + #[test] + fn test_sbox_matches_figure_1() { + // Exhaustive: all 256 inputs. 16 byte positions per pass, so sixteen passes cover them + // all, with the byte value `16 * pass + position` at each position. This is what makes the + // generated gate list trustworthy, so it must stay exhaustive. + for pass in 0..16u16 { + let mut q: Planes = core::array::from_fn(|w| { + let base = 16 * pass + 2 * w as u16; + u16::from_le_bytes([base as u8, base as u8 + 1]) + }); + let inputs = q; + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + for (w, (got, input)) in q.iter().zip(inputs.iter()).enumerate() { + for (byte, expected) in got + .to_le_bytes() + .iter() + .zip(input.to_le_bytes().iter().map(|&b| SBOX[b as usize])) + { + assert_eq!(*byte, expected, "pass {pass}, word {w}, input {input:#06x}"); + } + } + } + } + + #[test] + fn test_tau_matches_the_table_form() { + // Four different words at once, against the byte-by-byte table lookup, twice. + for mut words in [ + [0xEF00_0000u32, 0x00EF_0000, 0x0000_EF00, 0x0000_00EF], + [0x0123_4567, 0x89AB_CDEF, 0xFFFF_FFFF, 0x0000_0000], + ] { + let expected: [u32; LANES] = core::array::from_fn(|i| tau_table(words[i])); + tau(&mut words); + assert_eq!(words, expected); + } + // Sec 6.2.1: the bytes are independent, most significant byte first. + let mut words = [0xEF00_0000u32, 0x00EF_0000, 0x0000_EF00, 0x0000_00EF]; + tau(&mut words); + assert_eq!(words[0], 0x84D6_D6D6); + assert_eq!(words[3], 0xD6D6_D684); + } + + #[test] + fn test_tau_lanes_are_independent() { + // Changing one word must not change any other word's output. + let base: [u32; LANES] = + core::array::from_fn(|i| 0x1111_1111u32.wrapping_mul(i as u32 + 1)); + let mut expected = base; + tau(&mut expected); + for lane in 0..LANES { + let mut words = base; + words[lane] ^= 0xA5A5_5A5A; + tau(&mut words); + for other in (0..LANES).filter(|&o| o != lane) { + assert_eq!(words[other], expected[other], "lane {lane} disturbed lane {other}"); + } + assert_eq!(words[lane], tau_table(base[lane] ^ 0xA5A5_5A5A)); + } + } +} diff --git a/crypto/sm4/src/schedule.rs b/crypto/sm4/src/schedule.rs new file mode 100644 index 000000000..c4cda36f6 --- /dev/null +++ b/crypto/sm4/src/schedule.rs @@ -0,0 +1,166 @@ +//! The key schedule (GB/T 32907-2016, as described in draft-ribose-cfrg-sm4-10 Sec 7.3), with the +//! family key `FK` (Sec 7.3.1) and constant key `CK` (Sec 7.3.2). +//! +//! The schedule is the 32 round keys `rk_0 .. rk_31`, one 32-bit word each, so 128 bytes. It is +//! computed once by [`expand`] and stored in a [`Secret`]. Decryption uses the same words in the +//! reverse order (Sec 7.2), so there is no second schedule: this crate stores the encryption order +//! and lets [`SM4Internal::decrypt_4blocks`](crate::hazmat::SM4Internal::decrypt_4blocks) index +//! it backwards, rather than expanding a second copy for decryption. +//! +//! Every word is big-endian, as the worked examples in Appendix A.1 require: with the Example 1 +//! key `01 23 45 67 ...`, `MK_0` is `0x01234567`. +//! +//! # Constant-time +//! +//! `T'` routes secret key material through the S-box, so the expansion needs the same treatment +//! as the cipher: [`tau`] here is the bit-sliced circuit of [`crate::sbox`], not a table. It costs +//! a full circuit evaluation per round key to substitute four bytes, which is wasteful, but it +//! happens 32 times per key rather than per block. + +use crate::LANES; +use crate::sbox::tau; +use bouncycastle_utils::secret::Secret; + +/// The 32 round keys, `(rk_0, rk_1, ..., rk_31)` (Sec 5). +pub(crate) type RoundKeys = [u32; 32]; + +/// The family key `FK = (FK_0, FK_1, FK_2, FK_3)` (Sec 7.3.1). +pub(crate) const FK: [u32; 4] = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]; + +/// The constant key `CK = (CK_0, ..., CK_31)` (Sec 7.3.2). +/// +/// Defined by `ck_{i,j} = (4i + j) x 7 (mod 256)` for byte `j` of `CK_i`; the table is the +/// spec's own listing of those values, extracted mechanically from the text of the draft, and +/// `test_ck_matches_its_defining_formula` re-derives every word. +pub(crate) const CK: [u32; 32] = [ + 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, + 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, + 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, + 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279, +]; + +/// `L'(B) = B xor (B <<< 13) xor (B <<< 23)` (Sec 6.2.2). +#[inline(always)] +fn l_prime(b: u32) -> u32 { + b ^ b.rotate_left(13) ^ b.rotate_left(23) +} + +/// `T'(.) = L'(tau(.))` (Sec 6.2): the permutation `T` with `L` replaced by `L'`. +/// +/// `tau` works on four words at once; the one word here is placed in every lane, and lane 0 is +/// read back. All four lanes then hold the same result, which `test_t_prime_lanes_agree` checks. +fn t_prime(z: u32) -> u32 { + let mut words = [z; LANES]; + tau(&mut words); + l_prime(words[0]) +} + +/// Expands a 128-bit key into the 32 round keys (Sec 7.3). +/// +/// Line by line against Sec 7.3: +/// +/// * `MK = (MK_0, MK_1, MK_2, MK_3)` -- the key as four big-endian words; +/// * `(K_0, K_1, K_2, K_3) = (MK_0 xor FK_0, MK_1 xor FK_1, MK_2 xor FK_2, MK_3 xor FK_3)`; +/// * for `i = 0, 1, ..., 31`: +/// `rk_i = K_{i+4} = K_i xor T'(K_{i+1} xor K_{i+2} xor K_{i+3} xor CK_i)`. +/// +/// Only the four most recent `K` words are ever read, so they are kept in a sliding four-word +/// window rather than a 36-word array; `test_round_keys_match_appendix_a_1_1` and +/// `test_round_keys_match_appendix_a_1_4` pin every one of the 32 outputs. +pub(crate) fn expand(key: &[u8; 16]) -> Secret { + // MK_0 .. MK_3, then K_i = MK_i xor FK_i. Held in a Secret so the window is scrubbed on return. + let mut k = Secret::<[u32; 4]>::new(); + for (i, word) in key.as_chunks::<4>().0.iter().enumerate() { + k[i] = u32::from_be_bytes(*word) ^ FK[i]; + } + + let mut rk = Secret::::new(); + for i in 0..32 { + // K_{i+4} = K_i xor T'(K_{i+1} xor K_{i+2} xor K_{i+3} xor CK_i) + let next = k[0] ^ t_prime(k[1] ^ k[2] ^ k[3] ^ CK[i]); + // rk_i = K_{i+4} + rk[i] = next; + // Slide the window: (K_{i+1}, K_{i+2}, K_{i+3}, K_{i+4}). + *k = [k[1], k[2], k[3], next]; + } + rk +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ck_matches_its_defining_formula() { + // Sec 7.3.2: ck_{i,j} = (4i + j) x 7 (mod 256) and + // CK_i = (ck_{i,0}, ck_{i,1}, ck_{i,2}, ck_{i,3}), with ck_{i,0} the most significant byte. + for (i, &ck) in CK.iter().enumerate() { + let bytes: [u8; 4] = core::array::from_fn(|j| (((4 * i + j) * 7) % 256) as u8); + assert_eq!(ck, u32::from_be_bytes(bytes), "CK_{i}"); + } + } + + #[test] + fn test_fk_matches_section_7_3_1() { + assert_eq!(FK, [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]); + } + + /// Appendix A.1.1 (GB/T 32907-2016 Example 1): key 0123456789ABCDEFFEDCBA9876543210. + #[test] + fn test_round_keys_match_appendix_a_1_1() { + let key = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, + 0x32, 0x10, + ]; + let expected: RoundKeys = [ + 0xF12186F9, 0x41662B61, 0x5A6AB19A, 0x7BA92077, 0x367360F4, 0x776A0C61, 0xB6BB89B3, + 0x24763151, 0xA520307C, 0xB7584DBD, 0xC30753ED, 0x7EE55B57, 0x6988608C, 0x30D895B7, + 0x44BA14AF, 0x104495A1, 0xD120B428, 0x73B55FA3, 0xCC874966, 0x92244439, 0xE89E641F, + 0x98CA015A, 0xC7159060, 0x99E1FD2E, 0xB79BD80C, 0x1D2115B0, 0x0E228AEB, 0xF1780C81, + 0x428D3654, 0x62293496, 0x01CF72E5, 0x9124A012, + ]; + let rk = expand(&key); + for (i, (got, want)) in rk.iter().zip(expected.iter()).enumerate() { + assert_eq!(got, want, "rk_{i}"); + } + } + + /// Appendix A.1.4 (Example 4): key FEDCBA98765432100123456789ABCDEF. + #[test] + fn test_round_keys_match_appendix_a_1_4() { + let key = [ + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, + ]; + let expected: RoundKeys = [ + 0x0D8CC1B4, 0xAC44F213, 0x188C0C40, 0x7537585E, 0x627646F5, 0x54D785AD, 0x51B96DEE, + 0x0C385958, 0x5E494992, 0x32F3FE04, 0x3A3A733D, 0x0EDFB91D, 0x6823CD6B, 0x40F7D825, + 0x4BD68EE5, 0x165A36C8, 0x56608984, 0x23F35FF4, 0x8B592B3E, 0x80F7388A, 0x0415C409, + 0xAFDF1370, 0xCF444772, 0x9AF9901F, 0xC457578C, 0x95701C60, 0x2B0F4EE1, 0x7F826139, + 0xFA37F8D9, 0xD18AF8CE, 0x5BD5D8C6, 0x711138B7, + ]; + let rk = expand(&key); + for (i, (got, want)) in rk.iter().zip(expected.iter()).enumerate() { + assert_eq!(got, want, "rk_{i}"); + } + } + + #[test] + fn test_t_prime_lanes_agree() { + // The single-word T' fills all four lanes with the same word; every lane must come back + // identical, or lane 0 would not be a valid answer. + for z in [0u32, 0xFFFF_FFFF, 0x0123_4567, 0xDEAD_BEEF, 0x8000_0001] { + let mut words = [z; LANES]; + tau(&mut words); + assert!(words.iter().all(|&w| w == words[0]), "lanes disagree for {z:#010x}"); + assert_eq!(l_prime(words[0]), t_prime(z)); + } + } + + #[test] + fn test_l_prime_is_the_documented_rotation_sum() { + // L'(B) = B xor (B <<< 13) xor (B <<< 23): a single set bit lands in exactly three places. + assert_eq!(l_prime(1), 1 | (1 << 13) | (1 << 23)); + assert_eq!(l_prime(0), 0); + } +} diff --git a/crypto/sm4/tests/alg_name_tests.rs b/crypto/sm4/tests/alg_name_tests.rs new file mode 100644 index 000000000..afe12485d --- /dev/null +++ b/crypto/sm4/tests/alg_name_tests.rs @@ -0,0 +1,160 @@ +//! What each public SM4 alias is: the algorithm it reports and the type it resolves to. +//! +//! Each alias reports its own algorithm through `Algorithm`, not the engine it is built on: +//! `SM4_CBC` is `"SM4_CBC"`, while `SM4Internal` stays `"SM4"`. The strength is the key's. The +//! direction and the padding are type parameters of one algorithm, not different algorithms, so +//! every combination of them must report the same name. + +use bouncycastle_cipher::modes::{CBC, CFB, CFB8, CTR, Cbc, Cfb, Cfb8, Ctr}; +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{ + BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, LANES, SM4_CBC, SM4_CFB, SM4_CFB8, SM4_CTR, SM4Key, +}; +use core::mem::size_of; + +/// Both directions of one algorithm report `name` at the 128-bit strength. +fn check(name: &str) { + assert_eq!(Enc::ALG_NAME, name); + assert_eq!(Dec::ALG_NAME, name); + assert_eq!(Enc::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit, "{name}"); + assert_eq!(Dec::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit, "{name}"); +} + +#[test] +fn cbc() { + check::, SM4_CBC<'_, Decrypting, PKCS7>>("SM4_CBC"); + check::, SM4_CBC<'_, Decrypting, NoPadding>>("SM4_CBC"); +} + +#[test] +fn cfb_cfb8_ctr() { + check::, SM4_CFB<'_, Decrypting>>("SM4_CFB"); + check::, SM4_CFB8<'_, Decrypting>>("SM4_CFB8"); + check::, SM4_CTR<'_, Decrypting>>("SM4_CTR"); +} + +#[test] +fn the_engine_keeps_its_own_name() { + assert_eq!(SM4Internal::ALG_NAME, "SM4"); + assert_eq!(SM4Internal::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); +} + +/// The aliases must resolve to exactly the types they claim to, at both directions, and be what +/// the key hands out. A type alias that quietly resolved to something else would still compile +/// everywhere it is used, so this pins the projection by asserting the layouts coincide with the +/// fully spelled-out types. +#[test] +fn the_aliases_name_the_expected_types() { + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Cbc<'_, SM4Internal, Encrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor< + Cbc<'_, SM4Internal, Decrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Cbc<'_, SM4Internal, Encrypting, BLOCK_LEN>, + NoPadding, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + // The two directions are genuinely different types, so the encryptor and the decryptor do not + // have to agree in size -- and here they do not, which is itself evidence the projection + // selected two different adapters rather than one. + assert_ne!( + size_of::>(), + size_of::>() + ); + + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + + // ...and the key hands out exactly those types. + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + let material = + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); + let mut rng = HashDRBG_SHA512::new_from_os(); + let key = SM4Key::>::from_key_material(&material).unwrap(); + let (enc, iv) = key.create_encryptor(&mut rng).unwrap(); + let _: SM4_CBC<'_, Encrypting, PKCS7> = enc; + let _: SM4_CBC<'_, Decrypting, PKCS7> = key.create_decryptor(&iv); + let key = SM4Key::::from_key_material(&material).unwrap(); + let _: SM4_CFB<'_, Encrypting> = key.create_encryptor(&mut rng).unwrap().0; + let key = SM4Key::::from_key_material(&material).unwrap(); + let _: SM4_CFB8<'_, Encrypting> = key.create_encryptor(&mut rng).unwrap().0; + let key = SM4Key::::from_key_material(&material).unwrap(); + let (enc, nonce) = key.create_encryptor(&mut rng).unwrap(); + let _: SM4_CTR<'_, Encrypting> = enc; + let _: SM4_CTR<'_, Decrypting> = key.create_decryptor(&nonce); +} + +/// The const parameters SM4 fills in are the ones the crate exports, CTR's nonce leaves the +/// four-byte counter the mode's maximum, and four lanes of four-byte words fill the 16 byte +/// positions of the `u16` planes. +#[test] +fn the_const_parameters_are_sm4s() { + assert_eq!(KEY_LEN, 16); + assert_eq!(BLOCK_LEN, 16); + assert_eq!(CTR_NONCE_LEN, 12, "a 12-byte nonce leaves the 4-byte counter CTR caps at"); + assert_eq!(LANES * 4, BLOCK_LEN, "four lanes of one word each fill the circuit's 16 bytes"); +} + +/// The engine is exactly the 32 round keys the crate docs quote as 128 bytes: no direction +/// flag, no round counter, no initialised marker. +#[test] +fn the_engine_is_exactly_the_round_keys() { + assert_eq!(size_of::(), 128); +} diff --git a/crypto/sm4/tests/bc_java_tests.rs b/crypto/sm4/tests/bc_java_tests.rs new file mode 100644 index 000000000..27f7077e0 --- /dev/null +++ b/crypto/sm4/tests/bc_java_tests.rs @@ -0,0 +1,97 @@ +//! The test vectors of Bouncy Castle Java's SM4 tests, run against this port. +//! +//! Two files in bc-java carry SM4 known answers, and both are reproduced here exactly as written +//! there: +//! +//! * `core/src/test/java/org/bouncycastle/crypto/test/SM4Test.java` -- a `BlockCipherVectorTest` +//! (key, plaintext, ciphertext, checked in both directions by `CipherTest`), and +//! `test1000000()`, which encrypts the plaintext 1,000,000 times in place, checks the result, +//! then decrypts it 1,000,000 times and checks the plaintext comes back. Its header credits the +//! vectors to , the Diffie-Ledin translation of the +//! standard. +//! * `prov/src/test/java/org/bouncycastle/jce/provider/test/SM4Test.java` -- the `cipherTests` +//! array `{ "128", key, plaintext, ciphertext }`, the same vector driven through the JCE +//! provider as `SM4/ECB/NoPadding`. +//! +//! These are GB/T 32907-2016's own Example 1 and Example 2, so they coincide with Appendix A.1.1 +//! through A.1.3 of draft-ribose-cfrg-sm4-10 in `gbt32907_tests.rs`. They are kept as a separate +//! file so that agreement with the source implementation is asserted in its own terms, including +//! the decryption half of `test1000000()`, which the draft does not print. That test is ignored in +//! unoptimised builds (see its attribute) and runs under `cargo test --release`. + +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_hex as hex; +use bouncycastle_sm4::BLOCK_LEN; +use bouncycastle_sm4::hazmat::SM4Internal; + +/// `SM4Test.tests[0]` and the provider `cipherTests`: `KeyParameter(Hex.decode(..))`. +const KEY: &str = "0123456789abcdeffedcba9876543210"; +/// The `BlockCipherVectorTest` input. +const PLAINTEXT: &str = "0123456789abcdeffedcba9876543210"; +/// The `BlockCipherVectorTest` output. +const CIPHERTEXT: &str = "681edf34d206965e86b3e94f536e4246"; +/// `test1000000()`: `cipher`. +const CIPHERTEXT_1000000: &str = "595298c7c6fd271f0402f804c33d3f66"; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn engine() -> SM4Internal { + let key = KeyMaterial::<16>::from_bytes_as_type(&block(KEY), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + SM4Internal::new(&key).expect("a valid SM4 key") +} + +/// `BlockCipherVectorTest`, encryption half: `engine.init(true, param)` then `processBlock`. +#[test] +fn block_cipher_vector_test_encrypts() { + let mut buf = block(PLAINTEXT); + engine().encrypt_block(&mut buf); + assert_eq!(buf, block(CIPHERTEXT), "SM4 failed encryption"); +} + +/// `BlockCipherVectorTest`, decryption half: `engine.init(false, param)` then `processBlock`. +#[test] +fn block_cipher_vector_test_decrypts() { + let mut buf = block(CIPHERTEXT); + engine().decrypt_block(&mut buf); + assert_eq!(buf, block(PLAINTEXT), "SM4 failed reversal"); +} + +/// The provider test's `cipherTests` entry is the same vector at "128" bits, through +/// `SM4/ECB/NoPadding`; ECB on one block is the bare permutation. +#[test] +fn provider_cipher_tests_entry() { + let mut buf = block(PLAINTEXT); + engine().encrypt_block(&mut buf); + assert_eq!(buf, block(CIPHERTEXT), "SM4 failed encryption"); + engine().decrypt_block(&mut buf); + assert_eq!(buf, block(PLAINTEXT), "SM4 failed decryption"); +} + +/// `test1000000()`, both halves: 1,000,000 encryptions in place must give `cipher`, and +/// 1,000,000 decryptions of that must give `plain` back. +/// +/// The decryption half is the part not printed in the specification, and it is the strongest +/// check of the reversed-round-key decryption path: one wrong `rk_{31-i}` would derail a million +/// chained blocks. +/// Gated to optimised builds: a million single-block calls through the four-lane circuit take +/// over a minute unoptimised. `cargo test --release -p bouncycastle-sm4` runs it. +#[cfg_attr(debug_assertions, ignore = "1,000,000 iterations; run with `cargo test --release`")] +#[test] +fn test_1000000() { + let engine = engine(); + let mut buf = block(PLAINTEXT); + + for _ in 0..1_000_000 { + engine.encrypt_block(&mut buf); + } + assert_eq!(buf, block(CIPHERTEXT_1000000), "1000000 encryption test failed"); + + for _ in 0..1_000_000 { + engine.decrypt_block(&mut buf); + } + assert_eq!(buf, block(PLAINTEXT), "1000000 decryption test failed"); +} diff --git a/crypto/sm4/tests/ccm_wycheproof.rs b/crypto/sm4/tests/ccm_wycheproof.rs new file mode 100644 index 000000000..9e59c3973 --- /dev/null +++ b/crypto/sm4/tests/ccm_wycheproof.rs @@ -0,0 +1,203 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/sm4_ccm_test.json`, through +//! the `CCM` hazmat mode traits over `SM4Internal`. +//! +//! The same suite as `crypto/aes/tests/ccm_wycheproof.rs` at SM4's one key length; see that file +//! for what the set probes and why most of its groups have no instantiation to dispatch to (the +//! mode's `NONCE_LEN` is `7..=13` bytes and its `TAG_LEN` one of `4, 6, .., 16`). The key carries +//! no AEAD role, so the cases run over the permutation directly, as `rfc8998_tests.rs` does. +//! Requires the Wycheproof repository at `../wycheproof`; absent, the test warns and passes. + +use bouncycastle_cipher::modes::CCM; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::{ + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, do_hazardous_operations, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_sm4::KEY_LEN; +use bouncycastle_sm4::hazmat::SM4Internal; + +/// Wraps the vector's raw key bytes, promoting them if `KeyMaterial`'s entropy heuristic declined +/// to call them a cipher key. +fn permutation(bytes: &[u8]) -> SM4Internal { + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("wycheproof key bytes fit the buffer"); + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) + }) + .expect("promoting a wycheproof test key"); + } + >::new(&key).expect("a wycheproof key") +} + +/// Runs one case at a fully-instantiated `(NONCE_LEN, TAG_LEN)`. A valid case must encrypt to +/// exactly `expected_ct`/`expected_tag` and decrypt back; an invalid one is checked on the +/// decrypt direction only and must fail the tag check with the buffer zeroized. +#[allow(clippy::too_many_arguments)] +fn run_case( + tc_id: u64, + key_bytes: &[u8], + nonce_bytes: &[u8], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) { + type Mode = CCM; + let perm = permutation(key_bytes); + let nonce: [u8; NONCE_LEN] = + nonce_bytes.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad nonce length")); + let tag: [u8; TAG_LEN] = + expected_tag.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad tag length")); + + if valid { + let mut ct = msg.to_vec(); + let got_tag = as AeadDeclaredEncryptMode< + SM4Internal, + KEY_LEN, + NONCE_LEN, + TAG_LEN, + TAG_LEN, + >>::encrypt_detached_in_place_nonce(&perm, &nonce, aad, &mut ct) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + assert_eq!(ct, expected_ct, "tcId {tc_id}: ciphertext mismatch"); + assert_eq!(got_tag, tag, "tcId {tc_id}: tag mismatch"); + } + + let mut plaintext = expected_ct.to_vec(); + match as AeadDeclaredDecryptMode< + SM4Internal, + KEY_LEN, + NONCE_LEN, + TAG_LEN, + TAG_LEN, + >>::decrypt_detached_in_place(&perm, &nonce, aad, &mut plaintext, &tag) + { + Ok(()) => { + assert!(valid, "tcId {tc_id}: an invalid vector decrypted and verified anyway"); + assert_eq!(plaintext, msg, "tcId {tc_id}: decrypted plaintext mismatch"); + } + Err(SymmetricCipherError::AEADTagCheckFailed) => { + assert!(!valid, "tcId {tc_id}: a valid vector failed its tag check"); + assert!( + plaintext.iter().all(|&b| b == 0), + "tcId {tc_id}: a failed tag check must leave the output buffer zeroized" + ); + } + Err(e) => panic!("tcId {tc_id}: unexpected CCM error: {e:?}"), + } +} + +/// Dispatches to one of the 7 (nonce) x 7 (tag) instantiations, or reports that the case's sizes +/// have none. +#[allow(clippy::too_many_arguments)] +fn dispatch( + tc_id: u64, + key_bytes: &[u8], + nonce_bytes: &[u8], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) -> bool { + if key_bytes.len() != KEY_LEN { + return false; + } + macro_rules! with_tag_len { + ($n:literal) => { + match expected_tag.len() { + 4 => run_case::<$n, 4>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 6 => run_case::<$n, 6>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 8 => run_case::<$n, 8>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 10 => run_case::<$n, 10>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 12 => run_case::<$n, 12>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 14 => run_case::<$n, 14>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + 16 => run_case::<$n, 16>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ), + _ => return false, + } + }; + } + match nonce_bytes.len() { + 7 => with_tag_len!(7), + 8 => with_tag_len!(8), + 9 => with_tag_len!(9), + 10 => with_tag_len!(10), + 11 => with_tag_len!(11), + 12 => with_tag_len!(12), + 13 => with_tag_len!(13), + _ => return false, + } + true +} + +#[test] +fn wycheproof_sm4_ccm_known_answer_tests() { + let Some(doc) = wycheproof_json("sm4_ccm_test.json") else { return }; + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some("SM4-CCM")); + + let groups = doc.get("testGroups").and_then(Value::as_array).expect("testGroups"); + + let mut run = 0usize; + let mut valid_count = 0usize; + let mut invalid_count = 0usize; + let mut unsupported_cases = 0usize; + + // Each case is dispatched on its own field lengths, not the group's declared sizes: a + // `Pseudorandom` group varies them per test. + for group in groups { + for test in group.get("tests").and_then(Value::as_array).expect("tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key_bytes = hex_field(test, "key", tc_id); + let nonce_bytes = hex_field(test, "iv", tc_id); + let aad = hex_field(test, "aad", tc_id); + let msg = hex_field(test, "msg", tc_id); + let ct = hex_field(test, "ct", tc_id); + let tag = hex_field(test, "tag", tc_id); + let valid = match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => true, + "invalid" => false, + other => panic!("tcId {tc_id}: unexpected result {other}"), + }; + + if dispatch(tc_id, &key_bytes, &nonce_bytes, &aad, &msg, &ct, &tag, valid) { + run += 1; + if valid { valid_count += 1 } else { invalid_count += 1 } + } else { + unsupported_cases += 1; + } + } + } + + println!( + "Wycheproof SM4-CCM: {run} cases run ({valid_count} valid, {invalid_count} invalid), \ + {unsupported_cases} cases not supported (no A.1 instantiation)" + ); + + // Guards against a silently-vacuous run, as the AES suite does. + assert!(run > 0, "expected at least some cases to be within A.1's dispatchable sets"); + assert!(valid_count > 0, "expected at least some valid cases to be run"); + assert!(invalid_count > 0, "expected at least some invalid (tag-failure) cases to be run"); + assert!( + unsupported_cases > 0, + "expected some of this adversarial set to be outside A.1's sets" + ); +} diff --git a/crypto/sm4/tests/common/mod.rs b/crypto/sm4/tests/common/mod.rs new file mode 100644 index 000000000..136d4ae68 --- /dev/null +++ b/crypto/sm4/tests/common/mod.rs @@ -0,0 +1,137 @@ +//! A table-driven reference SM4, for cross-checking the constant-time engine. +//! +//! This is BC Java's `SM4Engine` transcribed as directly as Rust allows: the 256-byte S-box table +//! of GB/T 32907-2016 Figure 1 (draft-ribose-cfrg-sm4-10 Sec 6.2.3), `FK`, `CK`, the `F0..F3` +//! unrolled rounds and the direction-dependent key expansion. It exists only in the tests, where +//! its cache-timing behaviour does not matter, and it is deliberately independent of the crate's +//! own code: it shares no modules with it, so agreement between the two is meaningful. +//! +//! It is itself checked against the specification's Example 1 in `reference_sanity`, so a bug +//! here cannot silently validate a bug in the engine. + +#![allow(dead_code)] + +/// Figure 1, "SM4 S-box Values", row-major; identical to BC Java's `Sbox`. +pub const SBOX: [u8; 256] = [ + 0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05, + 0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99, + 0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62, + 0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6, + 0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8, + 0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35, + 0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87, + 0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e, + 0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1, + 0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3, + 0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f, + 0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51, + 0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8, + 0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0, + 0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84, + 0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48, +]; + +const CK: [u32; 32] = [ + 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, + 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, + 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, + 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279, +]; + +const FK: [u32; 4] = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]; + +/// `SM4Engine.tau`: the table lookup, byte by byte. +fn tau(a: u32) -> u32 { + let b0 = SBOX[((a >> 24) & 0xff) as usize] as u32; + let b1 = SBOX[((a >> 16) & 0xff) as usize] as u32; + let b2 = SBOX[((a >> 8) & 0xff) as usize] as u32; + let b3 = SBOX[(a & 0xff) as usize] as u32; + (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 +} + +fn l_ap(b: u32) -> u32 { + b ^ b.rotate_left(13) ^ b.rotate_left(23) +} +fn t_ap(z: u32) -> u32 { + l_ap(tau(z)) +} +fn l(b: u32) -> u32 { + b ^ b.rotate_left(2) ^ b.rotate_left(10) ^ b.rotate_left(18) ^ b.rotate_left(24) +} +fn t(z: u32) -> u32 { + l(tau(z)) +} + +/// `SM4Engine.expandKey(forEncryption, key)`. +fn expand_key(for_encryption: bool, key: &[u8; 16]) -> [u32; 32] { + let mut rk = [0u32; 32]; + let mut k = [0u32; 4]; + for i in 0..4 { + let mk = u32::from_be_bytes([key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]]); + k[i] = mk ^ FK[i]; + } + if for_encryption { + rk[0] = k[0] ^ t_ap(k[1] ^ k[2] ^ k[3] ^ CK[0]); + rk[1] = k[1] ^ t_ap(k[2] ^ k[3] ^ rk[0] ^ CK[1]); + rk[2] = k[2] ^ t_ap(k[3] ^ rk[0] ^ rk[1] ^ CK[2]); + rk[3] = k[3] ^ t_ap(rk[0] ^ rk[1] ^ rk[2] ^ CK[3]); + for i in 4..32 { + rk[i] = rk[i - 4] ^ t_ap(rk[i - 3] ^ rk[i - 2] ^ rk[i - 1] ^ CK[i]); + } + } else { + rk[31] = k[0] ^ t_ap(k[1] ^ k[2] ^ k[3] ^ CK[0]); + rk[30] = k[1] ^ t_ap(k[2] ^ k[3] ^ rk[31] ^ CK[1]); + rk[29] = k[2] ^ t_ap(k[3] ^ rk[31] ^ rk[30] ^ CK[2]); + rk[28] = k[3] ^ t_ap(rk[31] ^ rk[30] ^ rk[29] ^ CK[3]); + for i in (0..28).rev() { + rk[i] = rk[i + 4] ^ t_ap(rk[i + 3] ^ rk[i + 2] ^ rk[i + 1] ^ CK[31 - i]); + } + } + rk +} + +/// `SM4Engine.processBlock` after `init(for_encryption, key)`. +fn process_block(for_encryption: bool, key: &[u8; 16], block: &mut [u8; 16]) { + let rk = expand_key(for_encryption, key); + let mut x = [0u32; 4]; + for i in 0..4 { + x[i] = u32::from_be_bytes([ + block[4 * i], + block[4 * i + 1], + block[4 * i + 2], + block[4 * i + 3], + ]); + } + let mut i = 0; + while i < 32 { + x[0] ^= t(x[1] ^ x[2] ^ x[3] ^ rk[i]); + x[1] ^= t(x[2] ^ x[3] ^ x[0] ^ rk[i + 1]); + x[2] ^= t(x[3] ^ x[0] ^ x[1] ^ rk[i + 2]); + x[3] ^= t(x[0] ^ x[1] ^ x[2] ^ rk[i + 3]); + i += 4; + } + block[0..4].copy_from_slice(&x[3].to_be_bytes()); + block[4..8].copy_from_slice(&x[2].to_be_bytes()); + block[8..12].copy_from_slice(&x[1].to_be_bytes()); + block[12..16].copy_from_slice(&x[0].to_be_bytes()); +} + +/// Reference encryption of one block. +pub fn encrypt_block(key: &[u8; 16], block: &mut [u8; 16]) { + process_block(true, key, block) +} + +/// Reference decryption of one block. +pub fn decrypt_block(key: &[u8; 16], block: &mut [u8; 16]) { + process_block(false, key, block) +} + +/// Deterministic pseudo-random bytes (xorshift), so the cross-check needs no RNG. +pub fn pseudo_random(seed: &mut u32) -> [u8; N] { + core::array::from_fn(|_| { + *seed ^= *seed << 13; + *seed ^= *seed >> 17; + *seed ^= *seed << 5; + (*seed >> 24) as u8 + }) +} diff --git a/crypto/sm4/tests/draft_modes_tests.rs b/crypto/sm4/tests/draft_modes_tests.rs new file mode 100644 index 000000000..9b9d680de --- /dev/null +++ b/crypto/sm4/tests/draft_modes_tests.rs @@ -0,0 +1,145 @@ +//! Known-answer tests from draft-ribose-cfrg-sm4-10 Appendix A.2, "Examples For Various Modes Of +//! Operations": the SM4-ECB vectors (A.2.1) and the SM4-CBC vectors (A.2.2). +//! +//! ECB applies the raw permutation to each block independently (Sec 8.3), so an ECB example +//! vector *is* a block-permutation test vector, and the two-block plaintext exercises the trait's +//! pair methods against published answers. (That is the only reason ECB appears in this crate; +//! see the crate docs on why you must not use it to encrypt anything.) +//! +//! The CBC vectors go through `SM4Key>`, the block-aligned key the padded `SM4_CBC` +//! alias wraps. There is no API for supplying an IV, so encryption is driven through the key's +//! `create_encryptor` with a [`FixedSeedRNG`] whose stream is the vector's IV, and the test +//! asserts the returned init data really is that IV before comparing any ciphertext. Decryption +//! takes the IV directly. +//! +//! The draft notes these vectors "can be verified using" Botan and OpenSSL. All values are +//! transcribed from the downloaded text of the draft. + +use bouncycastle_cipher::modes::{Blocks, CBC}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, SM4Key}; + +/// The two plaintext blocks shared by every A.2 example. +const PLAINTEXT: &str = "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbb"; +/// The IV shared by every A.2 IV-based example. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// A.2.x Example 1 key. +const KEY_1: &str = "0123456789abcdeffedcba9876543210"; +/// A.2.x Example 2 key. +const KEY_2: &str = "fedcba98765432100123456789abcdef"; + +/// A.2.1.1 SM4-ECB Example 1 ciphertext. +const ECB_CT_1: &str = "5ec8143de509cff7b5179f8f474b86192f1d305a7fb17df985f81c8482192304"; +/// A.2.1.2 SM4-ECB Example 2 ciphertext. +const ECB_CT_2: &str = "c5876897e4a59bbba72a10c83872245b12dd90bc2d200692b529a4155ac9e600"; +/// A.2.2.1 SM4-CBC Example 1 ciphertext. +const CBC_CT_1: &str = "78ebb11cc40b0a48312aaeb2040244cb4cb7016951909226979b0d15dc6a8f6d"; +/// A.2.2.2 SM4-CBC Example 2 ciphertext. +const CBC_CT_2: &str = "0d3a6ddc2d21c698857215587b7bb59a91f2c147911a4144665e1fa1d40bae38"; + +fn bytes(hex_str: &str) -> [u8; N] { + hex::decode(hex_str).expect("valid hex").try_into().expect("expected length") +} + +fn blocks(hex_str: &str) -> [[u8; BLOCK_LEN]; 2] { + let flat: [u8; 32] = bytes(hex_str); + let (chunks, _) = flat.as_chunks::(); + [chunks[0], chunks[1]] +} + +fn key_material(hex_str: &str) -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&bytes::<16>(hex_str), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +// ---- A.2.1 SM4-ECB ------------------------------------------------------------------------ + +fn check_ecb(section: &str, key: &str, expected: &str) { + let sm4 = SM4Internal::new(&key_material(key)).unwrap(); + let pt = blocks(PLAINTEXT); + let ct = blocks(expected); + + // Block by block: Sec 8.3.1, C_i = SM4_Enc(K, P_i). + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let mut b = *p; + sm4.encrypt_block(&mut b); + assert_eq!(&b, c, "{section} encrypt, block #{}", i + 1); + + let mut b = *c; + sm4.decrypt_block(&mut b); + assert_eq!(&b, p, "{section} decrypt, block #{}", i + 1); + } + + // The pair methods, in one call each. + let mut pair = pt; + sm4.encrypt_2blocks(&mut pair); + assert_eq!(pair, ct, "{section} encrypt_2blocks"); + sm4.decrypt_2blocks(&mut pair); + assert_eq!(pair, pt, "{section} decrypt_2blocks"); +} + +#[test] +fn a_2_1_1_ecb_example_1() { + check_ecb("A.2.1.1", KEY_1, ECB_CT_1); +} + +#[test] +fn a_2_1_2_ecb_example_2() { + check_ecb("A.2.1.2", KEY_2, ECB_CT_2); +} + +// ---- A.2.2 SM4-CBC ------------------------------------------------------------------------ + +fn check_cbc(section: &str, key: &str, expected: &str) { + // The A.2.2 vectors are whole blocks, so they go through the block pair of a `CBC` + // key rather than the padded `SM4_CBC` alias, which would append a padding block to this + // already-aligned message. `padded_alias_tests.rs` covers the alias. + let key = SM4Key::>::from_key_material(&key_material(key)).unwrap(); + let iv: [u8; 16] = bytes(IV); + let pt: [u8; 32] = bytes(PLAINTEXT); + let ct: [u8; 32] = bytes(expected); + + // Encrypt, both blocks in one call, under the vector's IV. + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + let mut data = pt; + enc.do_encrypt_blocks_inplace(data.as_chunks_mut::().0).unwrap(); + assert_eq!(data, ct, "{section} encrypt, two blocks in one call"); + + // Encrypt one block at a time. + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); + let mut data = pt; + for block in data.as_chunks_mut::().0.iter_mut() { + enc.do_encrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + } + assert_eq!(data, ct, "{section} encrypt, one block at a time"); + + // Decrypt with the IV as init data: one shot, and streaming. + let mut data = ct; + key.decrypt_inplace(&iv, &mut data).unwrap(); + assert_eq!(data, pt, "{section} decrypt, one shot"); + + let mut dec = key.create_decryptor(&iv); + let mut data = ct; + for block in data.as_chunks_mut::().0.iter_mut() { + dec.do_decrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + } + assert_eq!(data, pt, "{section} decrypt, one block at a time"); +} + +#[test] +fn a_2_2_1_cbc_example_1() { + check_cbc("A.2.2.1", KEY_1, CBC_CT_1); +} + +#[test] +fn a_2_2_2_cbc_example_2() { + check_cbc("A.2.2.2", KEY_2, CBC_CT_2); +} diff --git a/crypto/sm4/tests/electronic_code_book_tests.rs b/crypto/sm4/tests/electronic_code_book_tests.rs new file mode 100644 index 000000000..1413e3e87 --- /dev/null +++ b/crypto/sm4/tests/electronic_code_book_tests.rs @@ -0,0 +1,16 @@ +//! `ElectronicCodeBook` trait conformance, via the shared test framework. +//! +//! The framework checks the properties every implementor must have -- both directions are +//! inverses, the permutation is injective, the pair methods are indistinguishable from two +//! single-block calls, and the key checks behave. `SM4Internal`'s pair and four-block methods +//! fill the circuit's lanes rather than calling the single-block path, so the equivalence is not +//! true by construction. + +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, KEY_LEN}; + +#[test] +fn sm4_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::(); +} diff --git a/crypto/sm4/tests/gbt32907_tests.rs b/crypto/sm4/tests/gbt32907_tests.rs new file mode 100644 index 000000000..301fbffec --- /dev/null +++ b/crypto/sm4/tests/gbt32907_tests.rs @@ -0,0 +1,199 @@ +//! Known-answer tests from GB/T 32907-2016, as reproduced in draft-ribose-cfrg-sm4-10 Appendix A.1. +//! +//! Examples 1 and 2 are the standard's Example 1 in each direction; Example 3 is its Example 2, +//! the 1,000,000-fold iterated encryption. Examples 4 through 6 are the draft's own second key, +//! in the same three shapes. Every value here is transcribed from the downloaded text of the +//! draft. +//! +//! The per-round `rk_i` and `X_i` columns that Appendix A.1 also prints are **not** tested here: +//! the round keys are deliberately not public API (a `Secret` field) and the round outputs are +//! internal to the round loop, so both are pinned in the `#[cfg(test)]` modules of +//! `src/schedule.rs` and `src/hazmat/sm4.rs`, where they can be reached. A round trip through +//! the cipher cannot check either -- a wrong `rk_i` is used by encryption and decryption alike. +//! +//! Key-handling behaviour (type, length, strength, `Debug` redaction) is tested here too. +//! +//! The two 1,000,000-iteration examples are ignored in unoptimised builds (see their attributes) +//! and run under `cargo test --release`. + +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_sm4::BLOCK_LEN; +use bouncycastle_sm4::hazmat::SM4Internal; + +/// Examples 1-3 key (and plaintext): `0123456789ABCDEFFEDCBA9876543210`. +const KEY_1: [u8; 16] = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, +]; +/// Examples 1-3 plaintext, which happens to equal the key. +const PT_1: [u8; BLOCK_LEN] = KEY_1; +/// A.1.1 ciphertext. +const CT_1: [u8; BLOCK_LEN] = [ + 0x68, 0x1E, 0xDF, 0x34, 0xD2, 0x06, 0x96, 0x5E, 0x86, 0xB3, 0xE9, 0x4F, 0x53, 0x6E, 0x42, 0x46, +]; +/// A.1.3 ciphertext after 1,000,000 encryptions. +const CT_1_ITERATED: [u8; BLOCK_LEN] = [ + 0x59, 0x52, 0x98, 0xC7, 0xC6, 0xFD, 0x27, 0x1F, 0x04, 0x02, 0xF8, 0x04, 0xC3, 0x3D, 0x3F, 0x66, +]; + +/// Examples 4-6 key: `FEDCBA98765432100123456789ABCDEF`. +const KEY_4: [u8; 16] = [ + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, +]; +/// Examples 4-6 plaintext: `000102030405060708090A0B0C0D0E0F`. +const PT_4: [u8; BLOCK_LEN] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +]; +/// A.1.4 ciphertext. +const CT_4: [u8; BLOCK_LEN] = [ + 0xF7, 0x66, 0x67, 0x8F, 0x13, 0xF0, 0x1A, 0xDE, 0xAC, 0x1B, 0x3E, 0xA9, 0x55, 0xAD, 0xB5, 0x94, +]; +/// A.1.6 ciphertext after 1,000,000 encryptions. +const CT_4_ITERATED: [u8; BLOCK_LEN] = [ + 0x37, 0x9A, 0x96, 0xD0, 0xA6, 0xA5, 0xA5, 0x06, 0x0F, 0xB4, 0x60, 0xC7, 0x5D, 0x18, 0x79, 0xED, +]; + +fn key_material(bytes: &[u8; 16]) -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +#[test] +fn a_1_1_example_1_encrypt() { + let sm4 = SM4Internal::new(&key_material(&KEY_1)).unwrap(); + let mut block = PT_1; + sm4.encrypt_block(&mut block); + assert_eq!(block, CT_1); +} + +#[test] +fn a_1_2_example_2_decrypt() { + let sm4 = SM4Internal::new(&key_material(&KEY_1)).unwrap(); + let mut block = CT_1; + sm4.decrypt_block(&mut block); + assert_eq!(block, PT_1); +} + +/// A.1.3: "encryption of a plaintext 1,000,000 times repeatedly, using a fixed encryption key". +/// Gated to optimised builds: a million single-block calls through the four-lane circuit take +/// over a minute unoptimised. `cargo test --release -p bouncycastle-sm4` runs it. +#[cfg_attr(debug_assertions, ignore = "1,000,000 iterations; run with `cargo test --release`")] +#[test] +fn a_1_3_example_3_one_million_encryptions() { + let sm4 = SM4Internal::new(&key_material(&KEY_1)).unwrap(); + let mut block = PT_1; + for _ in 0..1_000_000 { + sm4.encrypt_block(&mut block); + } + assert_eq!(block, CT_1_ITERATED); +} + +#[test] +fn a_1_4_example_4_encrypt() { + let sm4 = SM4Internal::new(&key_material(&KEY_4)).unwrap(); + let mut block = PT_4; + sm4.encrypt_block(&mut block); + assert_eq!(block, CT_4); +} + +#[test] +fn a_1_5_example_5_decrypt() { + let sm4 = SM4Internal::new(&key_material(&KEY_4)).unwrap(); + let mut block = CT_4; + sm4.decrypt_block(&mut block); + assert_eq!(block, PT_4); +} + +/// A.1.6: Example 4 iterated 1,000,000 times. +/// Gated to optimised builds: a million single-block calls through the four-lane circuit take +/// over a minute unoptimised. `cargo test --release -p bouncycastle-sm4` runs it. +#[cfg_attr(debug_assertions, ignore = "1,000,000 iterations; run with `cargo test --release`")] +#[test] +fn a_1_6_example_6_one_million_encryptions() { + let sm4 = SM4Internal::new(&key_material(&KEY_4)).unwrap(); + let mut block = PT_4; + for _ in 0..1_000_000 { + sm4.encrypt_block(&mut block); + } + assert_eq!(block, CT_4_ITERATED); +} + +/// The two-block trait methods must agree with the single-block known answers, in both slots. +#[test] +fn pair_methods_reproduce_the_appendix_ciphertexts() { + let sm4 = SM4Internal::new(&key_material(&KEY_1)).unwrap(); + + let other = [0xAAu8; 16]; + let mut other_alone = other; + sm4.encrypt_block(&mut other_alone); + + let mut pair = [PT_1, other]; + BlockPermutation::encrypt_2blocks(&sm4, &mut pair); + assert_eq!(pair, [CT_1, other_alone]); + + BlockPermutation::decrypt_2blocks(&sm4, &mut pair); + assert_eq!(pair, [PT_1, other]); +} + +// ---- key handling ----------------------------------------------------------------------- + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + // KeyType::Seed is not a cipher key: a seed reused directly as an SM4 key is a real mistake + // and the type system tracks enough to catch it. + let key = KeyMaterial::<16>::from_bytes_as_type(&KEY_1, KeyType::Seed).unwrap(); + assert!(SM4Internal::new(&key).is_err()); + + let key = KeyMaterial::<16>::from_bytes_as_type(&KEY_1, KeyType::MACKey).unwrap(); + assert!(SM4Internal::new(&key).is_err()); +} + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + // The capacity is right but only part of it is populated, so `key_len()` disagrees with the + // one SM4 key length. This is the one length error the const generic cannot catch by itself. + let key = + KeyMaterial::<16>::from_bytes_as_type(&KEY_1[..8], KeyType::SymmetricCipherKey).unwrap(); + assert!(SM4Internal::new(&key).is_err()); +} + +#[test] +fn a_key_carrying_too_low_a_security_strength_is_rejected() { + // A full-length key whose material was only ever derived at a lower security strength must + // not be usable at the strength its length implies. `from_bytes_as_type` tags a 16-byte key + // as 128-bit, so lower it deliberately -- lowering does not need a hazardous closure, only + // raising does. + let mut key = key_material(&KEY_1); + assert_eq!(key.security_strength(), SecurityStrength::_128bit); + + key.set_security_strength(SecurityStrength::_112bit).unwrap(); + assert!( + SM4Internal::new(&key).is_err(), + "SM4 must reject a 16-byte key only derived at the 112-bit strength" + ); + + // The same key at its full strength is fine, so the rejection is about the strength tag and + // not about anything else having gone wrong with the key. + assert!(SM4Internal::new(&key_material(&KEY_1)).is_ok()); +} + +#[test] +fn an_all_zero_key_is_rejected_unless_explicitly_promoted() { + // `KeyMaterial` tags an all-zero buffer `Zeroized`, and `SM4Internal::new` refuses it as the + // wrong key type. BC Java's `SM4Test` runs its generic `CipherTest` checks under + // `new byte[16]`; a caller who really wants that key must opt in through + // `do_hazardous_operations`, as the CLI does. + let key = + KeyMaterial::<16>::from_bytes_as_type(&[0u8; 16], KeyType::SymmetricCipherKey).unwrap(); + assert_eq!(key.key_type(), KeyType::Zeroized); + assert!(SM4Internal::new(&key).is_err()); +} + +#[test] +fn debug_does_not_print_the_round_keys() { + // The schedule is secret; `Debug` must not be a way to leak it. + let sm4 = SM4Internal::new(&key_material(&KEY_1)).unwrap(); + let rendered = format!("{sm4:?}"); + assert_eq!(rendered, "SM4"); +} diff --git a/crypto/sm4/tests/gcm_wycheproof.rs b/crypto/sm4/tests/gcm_wycheproof.rs new file mode 100644 index 000000000..97c42c861 --- /dev/null +++ b/crypto/sm4/tests/gcm_wycheproof.rs @@ -0,0 +1,137 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/sm4_gcm_test.json`, through +//! `SM4Key`'s detached one-shots. +//! +//! The same suite as `crypto/aes/tests/gcm_wycheproof.rs` at SM4's one key length; see that file +//! for what the set probes, why only the 96-bit-IV groups have an instantiation to dispatch to +//! (`Gcm` fixes the nonce at `GCM_NONCE_LEN`), and the seeded-RNG technique that makes the key +//! draw the vector's IV. Requires the Wycheproof repository at `../wycheproof`; absent, the test +//! warns and passes. + +use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_sm4::{KEY_LEN, SM4Key}; + +/// The tag length every group in the file uses. +const TAG_LEN: usize = 16; + +/// The key for the vector's raw key bytes, promoted if `KeyMaterial`'s entropy heuristic +/// declined to call them a cipher key. +fn gcm_key(bytes: &[u8]) -> SM4Key { + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("wycheproof key bytes fit the buffer"); + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) + }) + .expect("promoting a wycheproof test key"); + } + SM4Key::from_key_material(&key).expect("a wycheproof key") +} + +/// Runs one case. A valid case must encrypt to exactly `expected_ct`/`expected_tag` under the +/// vector's IV and decrypt back; an invalid one is checked on the decrypt direction only and must +/// fail the tag check with the buffer zeroized. +#[allow(clippy::too_many_arguments)] +fn run_case( + tc_id: u64, + key_bytes: &[u8], + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) { + let key = gcm_key(key_bytes); + let tag: [u8; TAG_LEN] = + expected_tag.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad tag length")); + + if valid { + let mut ct = vec![0u8; msg.len()]; + let (written, got_iv, got_tag) = key + .encrypt_detached_out(&mut FixedSeedRNG::::new(iv), aad, msg, &mut ct) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + assert_eq!(got_iv, iv, "tcId {tc_id}: the seeded RNG must reproduce the vector's IV"); + assert_eq!(written, msg.len(), "tcId {tc_id}: encrypt_detached writes exactly msg.len()"); + assert_eq!(ct, expected_ct, "tcId {tc_id}: ciphertext mismatch"); + assert_eq!(got_tag, tag, "tcId {tc_id}: tag mismatch"); + } + + // Pre-filled with a non-zero byte, so a buffer the decryptor never wrote is not mistaken for + // one it zeroized. + let mut plaintext = vec![0xEEu8; expected_ct.len()]; + match key.decrypt_detached_out(&iv, aad, expected_ct, &tag, &mut plaintext) { + Ok(n) => { + assert!(valid, "tcId {tc_id}: an invalid vector decrypted and verified anyway"); + plaintext.truncate(n); + assert_eq!(plaintext, msg, "tcId {tc_id}: decrypted plaintext mismatch"); + } + Err(SymmetricCipherError::AEADTagCheckFailed) => { + assert!(!valid, "tcId {tc_id}: a valid vector failed its tag check"); + assert!( + plaintext.iter().all(|&b| b == 0), + "tcId {tc_id}: a failed tag check must leave the output buffer zeroized" + ); + } + Err(e) => panic!("tcId {tc_id}: unexpected GCM error: {e:?}"), + } +} + +#[test] +fn wycheproof_sm4_gcm_known_answer_tests() { + let Some(doc) = wycheproof_json("sm4_gcm_test.json") else { return }; + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some("SM4-GCM")); + + let groups = doc.get("testGroups").and_then(Value::as_array).expect("testGroups"); + + let mut run = 0usize; + let mut valid_count = 0usize; + let mut invalid_count = 0usize; + let mut unsupported_cases = 0usize; + + // A case is dispatched on its actual `iv` length, since the nonce length is fixed by the type. + for group in groups { + for test in group.get("tests").and_then(Value::as_array).expect("tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key_bytes = hex_field(test, "key", tc_id); + let iv_bytes = hex_field(test, "iv", tc_id); + let aad = hex_field(test, "aad", tc_id); + let msg = hex_field(test, "msg", tc_id); + let ct = hex_field(test, "ct", tc_id); + let tag = hex_field(test, "tag", tc_id); + let valid = match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => true, + "invalid" => false, + other => panic!("tcId {tc_id}: unexpected result {other}"), + }; + + let iv = <[u8; GCM_NONCE_LEN]>::try_from(iv_bytes.as_slice()); + match iv { + Ok(iv) if key_bytes.len() == KEY_LEN && tag.len() == TAG_LEN => { + run_case(tc_id, &key_bytes, iv, &aad, &msg, &ct, &tag, valid); + run += 1; + if valid { valid_count += 1 } else { invalid_count += 1 } + } + _ => unsupported_cases += 1, + } + } + } + + println!( + "Wycheproof SM4-GCM: {run} cases run ({valid_count} valid, {invalid_count} invalid), \ + {unsupported_cases} cases not supported (no 96-bit-IV instantiation)" + ); + + // Guards against a silently-vacuous run, as the AES suite does. + assert!(run > 0, "expected the 96-bit-IV groups to be dispatchable"); + assert!(valid_count > 0, "expected at least some valid cases to be run"); + assert!(invalid_count > 0, "expected at least some invalid (tag-failure) cases to be run"); + assert!(unsupported_cases > 0, "expected the other-IV-length cases to be outside Gcm's shape"); +} diff --git a/crypto/sm4/tests/mode_key_tests.rs b/crypto/sm4/tests/mode_key_tests.rs new file mode 100644 index 000000000..3667b9f1f --- /dev/null +++ b/crypto/sm4/tests/mode_key_tests.rs @@ -0,0 +1,160 @@ +//! The SM4 key through the shared conformance suites: construction policy for every mode a key +//! can carry, then the block, symmetric, stream and AEAD suites. + +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8, CMAC, CTR, GCM, ModeNames}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core_test_framework::aead::{ + TestFrameworkAEADCipher, TestFrameworkAEADTaggedLayout, +}; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{KEY_LEN, SM4Key}; + +fn material() -> KeyMaterial { + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid key") +} + +fn key>() -> SM4Key { + SM4Key::from_key_material(&material()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// The construction suite -- key type, strength and `generate` -- for the mode `M`, against the +/// name the permutation gives that mode. +fn construction_suite>(name: &'static str) { + TestFrameworkFromKeyMaterial::new().test::>(name, SecurityStrength::_128bit); +} + +#[test] +fn every_mode_key_enforces_the_material_policy() { + construction_suite::>(SM4Internal::CBC_ALG_NAME); + construction_suite::>(SM4Internal::CBC_ALG_NAME); + construction_suite::>(SM4Internal::CBC_ALG_NAME); + construction_suite::>(SM4Internal::ECB_ALG_NAME); + construction_suite::(SM4Internal::CFB_ALG_NAME); + construction_suite::(SM4Internal::CFB8_ALG_NAME); + construction_suite::(SM4Internal::CTR_ALG_NAME); + construction_suite::(SM4Internal::GCM_ALG_NAME); + construction_suite::>(SM4Internal::GCM_ALG_NAME); +} + +/// A key under a MAC mode has no role here, and its `KEY_TYPE` is the `MACKey` the mode asks +/// for: cipher material is refused for its type, and MAC material by the permutation, so the +/// constructor never contradicts the constant and `generate` fails the same way. +#[test] +fn a_mac_mode_key_refuses_every_material_consistently() { + type MacKey = SM4Key>; + assert_eq!(MacKey::KEY_TYPE, KeyType::MACKey); + let refused = |r: Result| { + assert!(matches!( + r, + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + }; + refused(MacKey::from_key_material(&material())); + let mac_material = + KeyMaterial::::from_bytes_as_type(&[0x42; KEY_LEN], KeyType::MACKey).unwrap(); + refused(MacKey::from_key_material(&mac_material)); + refused(MacKey::generate(&mut rng())); +} + +#[test] +fn cbc_blocks_key_passes_the_block_cipher_framework() { + TestFrameworkBlockCipher::new().test_key::<16, 16, 16, _>(&key::>(), &mut rng()); +} + +#[test] +fn padded_keys_pass_the_symmetric_cipher_framework() { + let framework = TestFrameworkSymmetricCipher::new(); + framework.test_key::<16, 16, 16, _>(&key::>(), &mut rng()); + framework.test_key::<16, 0, 16, _>(&key::>(), &mut rng()); +} + +/// `NoPadding` turns the adapters into an alignment check: the framework is told so, and asserts +/// that every other length is refused. +#[test] +fn no_padding_key_passes_the_symmetric_cipher_framework() { + let mut framework = TestFrameworkSymmetricCipher::new(); + framework.required_alignment = 16; + framework.test_key::<16, 16, 16, _>(&key::>(), &mut rng()); +} + +#[test] +fn stream_mode_keys_pass_the_stream_cipher_framework() { + let framework = TestFrameworkStreamCipher::new(); + framework.test_key::<16, 16, _>(&key::(), &mut rng()); + framework.test_key::<16, 16, _>(&key::(), &mut rng()); + framework.test_key::<16, 12, _>(&key::(), &mut rng()); +} + +/// The AEAD suite, which reaches the no-AAD symmetric contract as the AEAD one with an empty +/// AAD, and the inline `ciphertext || tag` layout at every byte-boundary edge. RFC 8998 +/// Appendix A.1 and the Wycheproof SM4-GCM set pin the algorithm itself beside this file. +#[test] +fn gcm_key_passes_the_aead_frameworks() { + TestFrameworkAEADCipher::new().test_key::<16, 12, 16, 16, _>(&key::(), &mut rng()); + TestFrameworkAEADTaggedLayout::new().test_key::<16, 12, 16, 16, _>(&key::()); +} + +/// The nonce is generated per encryption, so the same plaintext gives different ciphertext, and +/// each still round-trips. +#[test] +fn each_gcm_encryption_gets_a_fresh_nonce() { + let data = *b"the quick brown fox jumps over the lazy dog!!!"; + let key = key::(); + let mut rng = rng(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let mut ct = [0u8; 46]; + let (_, nonce, tag) = key.encrypt_detached_out(&mut rng, b"aad", &data, &mut ct).unwrap(); + assert!(seen.insert(nonce), "nonce repeated across encryptions"); + let mut pt = [0u8; 46]; + key.decrypt_detached_out(&nonce, b"aad", &ct, &tag, &mut pt).unwrap(); + assert_eq!(pt, data); + } +} + +/// The sizes the docs quote: the schedule alone for the unauthenticated modes, plus the hash +/// subkey under `GCM`. +#[test] +fn key_sizes_are_the_documented_ones() { + use core::mem::size_of; + assert_eq!(size_of::>(), 128); + assert_eq!(size_of::>(), 144); +} + +/// `material()` with one bit of its last byte flipped. +fn material_flipped() -> KeyMaterial { + let mut bytes: [u8; KEY_LEN] = + core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + bytes[KEY_LEN - 1] ^= 1; + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid key") +} + +/// Equality is over the expanded schedule, so it follows the material: the same bytes compare +/// equal, as does a clone, and a single flipped bit does not. Keys have no `Debug`, hence +/// `assert!` over `==`. +#[test] +fn keys_compare_equal_exactly_when_their_material_does() { + let ctr_key = key::(); + assert!(ctr_key == ctr_key.clone()); + assert!(ctr_key == key::()); + assert!(ctr_key != SM4Key::::from_key_material(&material_flipped()).unwrap()); +} diff --git a/crypto/sm4/tests/padded_alias_tests.rs b/crypto/sm4/tests/padded_alias_tests.rs new file mode 100644 index 000000000..cf5cdf826 --- /dev/null +++ b/crypto/sm4/tests/padded_alias_tests.rs @@ -0,0 +1,93 @@ +//! The padded CBC mode through the SM4 key. +//! +//! The alias is only a type alias, so what is worth testing is that both parameters actually +//! select: the direction picks the encryptor or the decryptor, and the padding scheme changes the +//! behaviour rather than being decorative. The mode and the padding layer are tested in +//! `bouncycastle-cipher`; this checks the wiring between them and the key that hands the alias +//! out. Which type the alias resolves to is pinned in `alg_name_tests.rs`. + +use bouncycastle_cipher::modes::CBC; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_sm4::{BLOCK_LEN, KEY_LEN, SM4Key}; + +fn material() -> KeyMaterial { + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// Round trips at lengths that need padding and lengths that do not. +#[test] +fn the_padded_key_round_trips() { + let key = SM4Key::>::from_key_material(&material()).unwrap(); + for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 127, 128] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (iv, ciphertext) = key.encrypt(&mut rng(), &plaintext).expect("encryption"); + assert_eq!(iv.len(), BLOCK_LEN); + assert_eq!( + ciphertext.len(), + (len / BLOCK_LEN + 1) * BLOCK_LEN, + "len {len}: PKCS7 pads up to the next whole block" + ); + let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// The padding parameter must actually select the scheme, not merely be carried around. +#[test] +fn the_padding_parameter_selects_the_scheme() { + let pkcs7_key = SM4Key::>::from_key_material(&material()).unwrap(); + let nopad_key = SM4Key::>::from_key_material(&material()).unwrap(); + + // A whole block: both schemes accept it, and they disagree about the length. + let aligned = [0x5Au8; BLOCK_LEN]; + let (_, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("PKCS7 accepts it"); + let (_, nopad) = nopad_key.encrypt(&mut rng(), &aligned).expect("NoPadding accepts it"); + assert_eq!(pkcs7.len(), 32, "PKCS7 adds a whole block of padding to aligned data"); + assert_eq!(nopad.len(), 16, "NoPadding adds nothing"); + + // Five bytes: PKCS7 pads it, NoPadding refuses rather than silently padding. + assert!(pkcs7_key.encrypt(&mut rng(), b"hello").is_ok(), "PKCS7 pads a partial block"); + assert!(nopad_key.encrypt(&mut rng(), b"hello").is_err(), "NoPadding must refuse it"); +} + +/// A ciphertext made under one scheme must not decrypt cleanly under the other. +#[test] +fn the_two_schemes_are_not_interchangeable() { + let pkcs7_key = SM4Key::>::from_key_material(&material()).unwrap(); + let nopad_key = SM4Key::>::from_key_material(&material()).unwrap(); + let aligned = [0x5Au8; BLOCK_LEN]; + let (iv, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("encryption"); + + // NoPadding hands back the padding block as if it were data: the silent mismatch the type + // parameter is there to prevent. + let as_nopad = nopad_key.decrypt(&iv, &pkcs7).expect("NoPadding cannot tell"); + assert_ne!(as_nopad, aligned); + assert_eq!(as_nopad.len(), 32, "it keeps the padding block as data"); + + assert_eq!(pkcs7_key.decrypt(&iv, &pkcs7).expect("decryption"), aligned); +} + +/// The IV is drawn per encryption, so the same plaintext gives different ciphertext. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = SM4Key::>::from_key_material(&material()).unwrap(); + let mut rng = rng(); + let plaintext = [0x77u8; 32]; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let (iv, ct) = key.encrypt(&mut rng, &plaintext).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions"); + assert_eq!(key.decrypt(&iv, &ct).unwrap(), plaintext); + } +} diff --git a/crypto/sm4/tests/reference_tests.rs b/crypto/sm4/tests/reference_tests.rs new file mode 100644 index 000000000..69cfe2546 --- /dev/null +++ b/crypto/sm4/tests/reference_tests.rs @@ -0,0 +1,121 @@ +//! Cross-check of the constant-time engine against a table-driven transcription of BC Java's +//! `SM4Engine` (`tests/common/mod.rs`), on many keys and blocks, in every lane, in both +//! directions. +//! +//! The known-answer files pin a handful of published values. This file pins *agreement with the +//! source implementation* over thousands of inputs, which is what catches a circuit or layout +//! error that happens not to be exercised by the published examples. + +mod common; + +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, LANES}; + +fn engine(key: &[u8; 16]) -> SM4Internal { + SM4Internal::new( + &KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"), + ) + .expect("a valid SM4 key") +} + +/// The reference itself reproduces GB/T 32907-2016 Example 1, so it can be trusted to judge. +#[test] +fn reference_sanity() { + let key = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, + 0x10, + ]; + let mut block = key; + common::encrypt_block(&key, &mut block); + assert_eq!( + block, + [ + 0x68, 0x1E, 0xDF, 0x34, 0xD2, 0x06, 0x96, 0x5E, 0x86, 0xB3, 0xE9, 0x4F, 0x53, 0x6E, + 0x42, 0x46 + ] + ); + common::decrypt_block(&key, &mut block); + assert_eq!(block, key); +} + +/// Single-block API against the reference: 256 keys, 8 blocks each, both directions. +#[test] +fn single_block_agrees_with_the_reference() { + let mut seed = 0x5EED_0001; + for _ in 0..256 { + let key: [u8; 16] = common::pseudo_random(&mut seed); + let sm4 = engine(&key); + for _ in 0..8 { + let block: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + + let mut ours = block; + let mut theirs = block; + sm4.encrypt_block(&mut ours); + common::encrypt_block(&key, &mut theirs); + assert_eq!(ours, theirs, "encrypt, key {key:02x?}, block {block:02x?}"); + + let mut ours = block; + let mut theirs = block; + sm4.decrypt_block(&mut ours); + common::decrypt_block(&key, &mut theirs); + assert_eq!(ours, theirs, "decrypt, key {key:02x?}, block {block:02x?}"); + } + } +} + +/// Four-lane API -- the natural unit -- against the reference: every lane must carry its own +/// block, undisturbed by the others. +#[test] +fn four_lanes_agree_with_the_reference() { + let mut seed = 0x5EED_0002; + for _ in 0..128 { + let key: [u8; 16] = common::pseudo_random(&mut seed); + let sm4 = engine(&key); + let blocks: [[u8; BLOCK_LEN]; LANES] = + core::array::from_fn(|_| common::pseudo_random(&mut seed)); + + let mut ours = blocks; + sm4.encrypt_4blocks(&mut ours); + for (lane, (o, b)) in ours.iter().zip(blocks.iter()).enumerate() { + let mut theirs = *b; + common::encrypt_block(&key, &mut theirs); + assert_eq!(*o, theirs, "encrypt lane {lane}"); + } + + sm4.decrypt_4blocks(&mut ours); + assert_eq!(ours, blocks, "decrypt_4blocks must invert encrypt_4blocks"); + + let mut ours = blocks; + sm4.decrypt_4blocks(&mut ours); + for (lane, (o, b)) in ours.iter().zip(blocks.iter()).enumerate() { + let mut theirs = *b; + common::decrypt_block(&key, &mut theirs); + assert_eq!(*o, theirs, "decrypt lane {lane}"); + } + } +} + +/// The two-block override against two single-block calls, in both slots. +#[test] +fn two_block_override_matches_two_single_calls() { + let mut seed = 0x5EED_0003; + for _ in 0..64 { + let key: [u8; 16] = common::pseudo_random(&mut seed); + let sm4 = engine(&key); + let a: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + let b: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + + let mut singly = [a, b]; + sm4.encrypt_block(&mut singly[0]); + sm4.encrypt_block(&mut singly[1]); + let mut paired = [a, b]; + sm4.encrypt_2blocks(&mut paired); + assert_eq!(paired, singly); + + sm4.decrypt_2blocks(&mut paired); + assert_eq!(paired, [a, b]); + } +} diff --git a/crypto/sm4/tests/rfc8998_tests.rs b/crypto/sm4/tests/rfc8998_tests.rs new file mode 100644 index 000000000..7ea3b6720 --- /dev/null +++ b/crypto/sm4/tests/rfc8998_tests.rs @@ -0,0 +1,104 @@ +//! The SM4-GCM and SM4-CCM test vectors of RFC 8998 Appendix A: A.1 through `SM4Key`, A.2 +//! through the `CCM<12, 16>` mode traits over `SM4Internal`. +//! +//! RFC 8998 Sec 3.5 defines `AEAD_SM4_GCM` and `AEAD_SM4_CCM` for TLS 1.3 as GCM and CCM over +//! SM4 with a 12-byte nonce and a 16-byte tag; its Appendix A gives one vector for each, the same +//! key, nonce, plaintext and associated data. The key carries the AEAD role for `GCM` and the +//! `SM4_GCM` alias, so A.1 runs through it; this crate aliases no CCM, so A.2 runs through the +//! `hazmat` mode traits over the permutation's key state, which is what a key under that mode +//! would forward to. All values are transcribed from the downloaded text of the RFC. + +use bouncycastle_cipher::modes::{CCM, GCM}; +use bouncycastle_core::hazmat::{ + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, Mode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_hex as hex; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{KEY_LEN, SM4Key}; + +const NONCE: &str = "00001234567800000000ABCD"; +const KEY: &str = "0123456789ABCDEFFEDCBA9876543210"; +const PLAINTEXT: &str = concat!( + "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", + "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", + "EEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF", + "EEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAA", +); +const AAD: &str = "FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2"; + +/// A.1 SM4-GCM. +const GCM_CT: &str = concat!( + "17F399F08C67D5EE19D0DC9969C4BB7D", + "5FD46FD3756489069157B282BB200735", + "D82710CA5C22F0CCFA7CBF93D496AC15", + "A56834CBCF98C397B4024A2691233B8D", +); +const GCM_TAG: &str = "83DE3541E4C2B58177E065A9BF7B62EC"; + +/// A.2 SM4-CCM. +const CCM_CT: &str = concat!( + "48AF93501FA62ADBCD414CCE6034D895", + "DDA1BF8F132F042098661572E7483094", + "FD12E518CE062C98ACEE28D95DF4416B", + "ED31A2F04476C18BB40C84A74B97DC5B", +); +const CCM_TAG: &str = "16842D4FA186F56AB33256971FA110F4"; + +type Sm4Ccm = CCM<12, 16>; + +fn bytes(hex_str: &str) -> [u8; N] { + hex::decode(hex_str).expect("valid hex").try_into().expect("expected length") +} + +fn material() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&bytes::(KEY), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +fn permutation() -> SM4Internal { + SM4Internal::new(&material()).expect("a valid SM4 key") +} + +#[test] +fn a_1_sm4_gcm() { + let key = SM4Key::::from_key_material(&material()).expect("a valid SM4 key"); + let nonce: [u8; 12] = bytes(NONCE); + let aad: [u8; 20] = bytes(AAD); + + let mut buf: [u8; 64] = bytes(PLAINTEXT); + let tag = key.encrypt_detached_in_place_nonce(&nonce, &aad, &mut buf).expect("encryption"); + assert_eq!(buf, bytes::<64>(GCM_CT), "A.1 ciphertext"); + assert_eq!(tag, bytes::<16>(GCM_TAG), "A.1 tag"); + + key.decrypt_detached_in_place(&nonce, &aad, &mut buf, &tag).expect("decryption"); + assert_eq!(buf, bytes::<64>(PLAINTEXT), "A.1 plaintext back"); + + let mut tampered = tag; + tampered[0] ^= 1; + assert!(key.decrypt_detached_in_place(&nonce, &aad, &mut buf, &tampered).is_err()); +} + +#[test] +fn a_2_sm4_ccm() { + let state = >::key_state(permutation()); + let nonce: [u8; 12] = bytes(NONCE); + let aad: [u8; 20] = bytes(AAD); + + let mut buf: [u8; 64] = bytes(PLAINTEXT); + let encrypt = >:: + encrypt_detached_in_place_nonce; + let tag = encrypt(&state, &nonce, &aad, &mut buf).expect("encryption"); + assert_eq!(buf, bytes::<64>(CCM_CT), "A.2 ciphertext"); + assert_eq!(tag, bytes::<16>(CCM_TAG), "A.2 tag"); + + let decrypt = >:: + decrypt_detached_in_place; + decrypt(&state, &nonce, &aad, &mut buf, &tag).expect("decryption"); + assert_eq!(buf, bytes::<64>(PLAINTEXT), "A.2 plaintext back"); + + let mut tampered = tag; + tampered[0] ^= 1; + assert!(decrypt(&state, &nonce, &aad, &mut buf, &tampered).is_err()); +} diff --git a/crypto/sm4/tests/stream_mode_alias_tests.rs b/crypto/sm4/tests/stream_mode_alias_tests.rs new file mode 100644 index 000000000..46e6e0fe1 --- /dev/null +++ b/crypto/sm4/tests/stream_mode_alias_tests.rs @@ -0,0 +1,185 @@ +//! Tests for the stream-mode aliases `SM4_CFB`, `SM4_CFB8` and `SM4_CTR`, through the keys that +//! hand them out. +//! +//! The aliases are only type aliases, so what is worth testing is that they behave as the mode +//! they claim to be: the direction selects the encryptor or the decryptor, the init data is the +//! length the mode defines, and the three modes are distinct from each other and from CBC. The +//! known-answer coverage is in `stream_mode_tests.rs`, which type each alias resolves to is +//! pinned in `alg_name_tests.rs`, and the modes themselves are tested in their own right in +//! `bouncycastle_cipher::modes`; this checks the wiring between them. + +use bouncycastle_cipher::modes::{CFB, CFB8, CTR}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, SM4Key}; + +fn material() -> KeyMaterial { + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid key") +} + +fn key>() -> SM4Key { + SM4Key::from_key_material(&material()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// Deterministic filler, so nothing here depends on an RNG. +fn filler(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +/// Every stream-mode key round-trips at any length, with the ciphertext exactly as long as the +/// plaintext -- the property that separates these three from the CBC key, which pads. +#[test] +fn every_stream_key_round_trips_at_any_length() { + fn check<'k, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + for len in [0usize, 1, 15, 16, 17, 64, 257] { + let plaintext = filler(len, len as u32); + let mut data = plaintext.clone(); + + let (written, init) = key.encrypt_inplace(&mut rng(), &mut data).expect("encryption"); + assert_eq!(written, len, "{name}, len {len}: bytes written"); + assert_eq!(data.len(), len, "{name}, len {len}: no padding is added"); + assert_eq!(init.len(), INIT_DATA_LEN, "{name}: init data length"); + + let read = key.decrypt_inplace(&init, &mut data).expect("decryption"); + assert_eq!(read, len, "{name}, len {len}: bytes read"); + assert_eq!(data, plaintext, "{name}, len {len}: round trip"); + } + } + + check::<16, _>("SM4_CFB", &key::()); + check::<16, _>("SM4_CFB8", &key::()); + check::<12, _>("SM4_CTR", &key::()); +} + +/// Streaming in arbitrary pieces gives the same answer as one call: a sequence of calls must be +/// equivalent to one over the concatenation, whatever the chunking. +#[test] +fn chunking_does_not_change_the_ciphertext() { + fn check<'k, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + let plaintext = filler(100, 99); + let mut whole = plaintext.clone(); + let (_, init) = key.encrypt_inplace(&mut rng(), &mut whole).expect("encryption"); + + for chunk in [1usize, 3, 7, 16, 33] { + let mut dec = key.create_decryptor(&init); + let mut data = whole.clone(); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).expect("decryption"); + } + assert_eq!(data, plaintext, "{name}: {chunk}-byte pieces should decrypt the same"); + } + } + + check::<16, _>("SM4_CFB", &key::()); + check::<16, _>("SM4_CFB8", &key::()); + check::<12, _>("SM4_CTR", &key::()); +} + +/// A fresh IV or nonce per encryption, so the same plaintext gives different ciphertext. For CTR +/// this is the whole security argument: a repeated nonce leaks the XOR of the two messages. +#[test] +fn each_encryption_gets_fresh_init_data() { + fn check<'k, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + let plaintext = [0x77u8; 32]; + let mut rng = rng(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let mut data = plaintext; + let (_, init) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); + assert!(seen.insert(init.to_vec()), "{name}: init data repeated across encryptions"); + key.decrypt_inplace(&init, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "{name}: round trip"); + } + } + + check::<16, _>("SM4_CFB", &key::()); + check::<16, _>("SM4_CFB8", &key::()); + check::<12, _>("SM4_CTR", &key::()); +} + +/// CFB128 and CFB8 are different, non-interoperable modes. Under the same key and IV their first +/// bytes agree -- both are `P_1 xor MSB_8(O_1)` of the same first output block -- and everything +/// after that diverges, because CFB8 re-enciphers after every byte while CFB128 waits for a block. +#[test] +fn cfb128_and_cfb8_are_not_interchangeable() { + let iv = [0x0Au8; BLOCK_LEN]; + let plaintext = filler(32, 12_345); + + let cfb_key = key::(); + let mut as_cfb = plaintext.clone(); + let (mut enc, got) = + cfb_key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("CFB128 init"); + assert_eq!(got, iv, "the pinned RNG should produce the chosen IV"); + enc.do_encrypt_inplace(&mut as_cfb).expect("CFB128"); + + let cfb8_key = key::(); + let mut as_cfb8 = plaintext.clone(); + let (mut enc8, got8) = + cfb8_key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("CFB8 init"); + assert_eq!(got8, iv); + enc8.do_encrypt_inplace(&mut as_cfb8).expect("CFB8"); + + assert_eq!(as_cfb[0], as_cfb8[0], "both take MSB_8 of the same first output block"); + assert_ne!(as_cfb[1..], as_cfb8[1..], "and diverge from the second byte on"); +} + +/// CTR is not CFB either, and its init data is shorter: 12 bytes against 16. +#[test] +fn ctr_is_not_cfb() { + let plaintext = filler(32, 999); + + let mut as_ctr = plaintext.clone(); + let (_, nonce) = key::().encrypt_inplace(&mut rng(), &mut as_ctr).expect("CTR"); + assert_eq!(nonce.len(), CTR_NONCE_LEN); + + let cfb_key = key::(); + let mut as_cfb = plaintext.clone(); + let (_, iv) = cfb_key.encrypt_inplace(&mut rng(), &mut as_cfb).expect("CFB128"); + assert_eq!(iv.len(), BLOCK_LEN); + + // A CFB decryptor handed a CTR ciphertext cannot recover the plaintext. + let mut misread = as_ctr.clone(); + let mut iv_from_nonce = [0u8; BLOCK_LEN]; + iv_from_nonce[..CTR_NONCE_LEN].copy_from_slice(&nonce); + cfb_key.decrypt_inplace(&iv_from_nonce, &mut misread).expect("decryption runs"); + assert_ne!(misread, plaintext, "CFB must not decrypt a CTR ciphertext"); +} diff --git a/crypto/sm4/tests/stream_mode_tests.rs b/crypto/sm4/tests/stream_mode_tests.rs new file mode 100644 index 000000000..f5af0985c --- /dev/null +++ b/crypto/sm4/tests/stream_mode_tests.rs @@ -0,0 +1,303 @@ +//! The stream modes of operation over SM4: the known-answer tests from draft-ribose-cfrg-sm4-10 +//! Appendix A.2 that cover them, through the `CFB`, `CFB8` and `CTR` keys. +//! +//! Appendix A.2 has an SM4-CFB section (A.2.4) and an SM4-CTR section (A.2.5), and the two are used +//! differently here because only one of them lines up with the key's parameters: +//! +//! * **A.2.4, SM4-CFB.** Straight known-answer tests through `SM4Key`. There is no API for +//! supplying an IV, so encryption is driven through the key's `create_encryptor` with a +//! [`FixedSeedRNG`] whose stream is the vector's IV, and the test asserts the returned init data +//! really is that IV before comparing any ciphertext. Decryption takes the IV directly. +//! * **A.2.5, SM4-CTR.** The draft's counter sequence makes the whole 16-byte IV the first counter +//! block and increments it as a unit; `Ctr` splits the block into a 12-byte nonce and a 4-byte +//! counter that starts at zero (see the `SM4_CTR` docs, and Sec 8.7, which leaves the sequence to +//! the caller: "the counter could be any sequence that does not repeat within the block size"). +//! The published counter blocks are therefore not ones the key can produce. The vectors instead +//! pin [`reference_ctr`], written straight from the Sec 8.7.1 equations over the permutation, and +//! `SM4Key` is then required to agree with that same reference on the counter blocks it does +//! produce. Everything internal to the mode -- the counter increment, the short final block, the +//! chunking -- is covered by the ACVP vectors in `bouncycastle_cipher::modes`. +//! +//! The draft defines SM4-CFB-8 (Sec 8.5.1) but publishes no example of it, so `SM4Key` is +//! pinned the same way: against [`reference_cfb8`], the Sec 8.5.2 equations at `s = 8` evaluated +//! over the permutation. +//! +//! All vector values are transcribed from the downloaded text of the draft. + +use bouncycastle_cipher::modes::{CFB, CFB8, CTR}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_sm4::hazmat::SM4Internal; +use bouncycastle_sm4::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, SM4Key}; + +/// A.2.x Example 1 key. +const KEY_1: &str = "0123456789abcdeffedcba9876543210"; +/// A.2.x Example 2 key. +const KEY_2: &str = "fedcba98765432100123456789abcdef"; +/// The IV shared by every A.2 IV-based example. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The two plaintext blocks shared by the A.2.4 SM4-CFB examples. +const CFB_PT: &str = "aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbb"; +/// A.2.4.1 SM4-CFB Example 1 ciphertext. +const CFB_CT_1: &str = "ac3236cb861dd316e6413b4e3c7524b769d4c54ed433b9a0346009beb37b2b3f"; +/// A.2.4.2 SM4-CFB Example 2 ciphertext. +const CFB_CT_2: &str = "5dcccd25a84ba16560d7f265887068490d9b86ff20c3bfe115ffa02ca6192cc5"; + +/// The four plaintext blocks shared by the A.2.5 SM4-CTR examples. +const CTR_PT: &str = concat!( + "aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb", + "ccccccccccccccccdddddddddddddddd", + "eeeeeeeeeeeeeeeeffffffffffffffff", + "aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb", +); +/// A.2.5.1 SM4-CTR Example 1 ciphertext. +const CTR_CT_1: &str = concat!( + "ac3236cb970cc20791364c395a1342d1", + "a3cbc1878c6f30cd074cce385cdd70c7", + "f234bc0e24c11980fd1286310ce37b92", + "6e02fcd0faa0baf38b2933851d824514", +); +/// A.2.5.2 SM4-CTR Example 2 ciphertext. +const CTR_CT_2: &str = concat!( + "5dcccd25b95ab07417a08512ee160e2f", + "8f661521cbbab44cc87138445bc29e5c", + "0ae0297205d62704173b21239b887f6c", + "8cb5b800917a2488284bde9e16ea2906", +); + +fn bytes(hex_str: &str) -> Vec { + hex::decode(hex_str).expect("valid hex") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let k: [u8; KEY_LEN] = bytes(hex_str).try_into().expect("a 16-byte key"); + KeyMaterial::::from_bytes_as_type(&k, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +fn iv_bytes() -> [u8; BLOCK_LEN] { + bytes(IV).try_into().expect("a 16-byte IV") +} + +/// Deterministic filler, so nothing here depends on an RNG. +fn filler(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- A.2.4 SM4-CFB, through the alias ------------------------------------------------------- + +/// Both A.2.4 examples, each encrypted under the vector's own IV and decrypted back, in one call +/// and then one block at a time. The chunked pass is the point of a stream cipher: the draft +/// defines the mode on the whole message, and `Cfb` must give the same answer whatever the +/// chunking. +fn check_cfb(section: &str, key_hex: &str, expected: &str) { + let key = SM4Key::::from_key_material(&key_material(key_hex)).expect("a valid key"); + let iv = iv_bytes(); + let pt = bytes(CFB_PT); + let ct = bytes(expected); + + // Encrypt in one call, under the vector's IV. + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encryption init"); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + let mut data = pt.clone(); + let written = enc.do_encrypt_inplace(&mut data).expect("encryption"); + assert_eq!(written, pt.len(), "{section}: a stream cipher writes exactly what it was given"); + assert_eq!(data, ct, "{section} encrypt, one call"); + + // Encrypt one block at a time: same answer. + let (mut enc, _) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encryption init"); + let mut data = pt.clone(); + for chunk in data.chunks_mut(BLOCK_LEN) { + enc.do_encrypt_inplace(chunk).expect("encryption"); + } + assert_eq!(data, ct, "{section} encrypt, one block at a time"); + + // Decrypt with the IV as init data: one shot, and streaming at an unaligned boundary. + let mut data = ct.clone(); + key.decrypt_inplace(&iv, &mut data).expect("decryption"); + assert_eq!(data, pt, "{section} decrypt, one shot"); + + let mut dec = key.create_decryptor(&iv); + let mut data = ct.clone(); + for chunk in data.chunks_mut(7) { + dec.do_decrypt_inplace(chunk).expect("decryption"); + } + assert_eq!(data, pt, "{section} decrypt, seven bytes at a time"); +} + +#[test] +fn a_2_4_1_cfb_example_1() { + check_cfb("A.2.4.1", KEY_1, CFB_CT_1); +} + +#[test] +fn a_2_4_2_cfb_example_2() { + check_cfb("A.2.4.2", KEY_2, CFB_CT_2); +} + +// ---- A.2.5 SM4-CTR, through a reference built from the equations ------------------------------ + +/// CTR straight from Sec 8.7.1, with the draft's own counter sequence. +/// +/// ```text +/// for i = 1 to n +/// O_i = SM4Encrypt(T_i) +/// C_i = P_i xor O_i (i < n) +/// C_n = P_n xor MSB(u, O_n) +/// ``` +/// +/// `T_1` is the caller's counter block and each `T_{i+1}` is `T_i` incremented as a 128-bit +/// big-endian integer, which is what reproduces the A.2.5 ciphertexts. Encryption and decryption +/// are the same operation, so one function serves both. +fn reference_ctr(key: &KeyMaterial, t1: [u8; BLOCK_LEN], data: &mut [u8]) { + let sm4 = SM4Internal::new(key).expect("a valid SM4 key"); + let mut t = t1; + for chunk in data.chunks_mut(BLOCK_LEN) { + // O_i = SM4Encrypt(T_i); a short final chunk takes MSB(u, O_n) by simply stopping early. + let mut o = t; + sm4.encrypt_block(&mut o); + for (d, k) in chunk.iter_mut().zip(o.iter()) { + *d ^= k; + } + // T_{i+1}: increment the whole block, carrying from the least significant byte. + for b in t.iter_mut().rev() { + *b = b.wrapping_add(1); + if *b != 0 { + break; + } + } + } +} + +/// The reference reproduces both published A.2.5 ciphertexts with `T_1 = IV`, in both directions. +/// This is what pins the reference; the key is then checked against it below. +#[test] +fn a_2_5_ctr_examples_pin_the_reference() { + for (section, key_hex, expected) in [("A.2.5.1", KEY_1, CTR_CT_1), ("A.2.5.2", KEY_2, CTR_CT_2)] + { + let key = key_material(key_hex); + let pt = bytes(CTR_PT); + let ct = bytes(expected); + + let mut data = pt.clone(); + reference_ctr(&key, iv_bytes(), &mut data); + assert_eq!(data, ct, "{section}: the reference should reproduce the published ciphertext"); + + reference_ctr(&key, iv_bytes(), &mut data); + assert_eq!(data, pt, "{section}: and encryption is its own inverse"); + } +} + +/// `SM4Key` agrees with the pinned reference on the counter blocks it does produce: `nonce` +/// followed by a four-byte counter starting at zero. Lengths are chosen to cross the block +/// boundary and to end on a short final block, which is where `MSB(u, O_n)` applies. +#[test] +fn the_ctr_key_agrees_with_the_reference() { + let material = key_material(KEY_1); + let key = SM4Key::::from_key_material(&material).expect("a valid key"); + let nonce: [u8; CTR_NONCE_LEN] = + bytes("000102030405060708090a0b").try_into().expect("a 12-byte nonce"); + + // The counter block the alias starts from: the nonce, then four zero bytes. + let mut t1 = [0u8; BLOCK_LEN]; + t1[..CTR_NONCE_LEN].copy_from_slice(&nonce); + + for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 64, 129] { + let plaintext = filler(len, len as u32); + + let mut expected = plaintext.clone(); + reference_ctr(&material, t1, &mut expected); + + let mut data = plaintext.clone(); + let (mut enc, got) = key + .create_encryptor(&mut FixedSeedRNG::::new(nonce)) + .expect("encryption init"); + assert_eq!(got, nonce, "the pinned RNG should produce the chosen nonce"); + enc.do_encrypt_inplace(&mut data).expect("encryption"); + assert_eq!(data, expected, "len {len}: SM4Key should match the reference"); + + // ...and decryption undoes it under the same nonce. + key.decrypt_inplace(&nonce, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "len {len}: round trip"); + } +} + +// ---- SM4-CFB-8, against the Sec 8.5.2 equations at s = 8 --------------------------------------- + +/// CFB straight from Sec 8.5.2 with `s = 8`, one byte per forward cipher call. +/// +/// ```text +/// I_1 = IV +/// I_i = LSB(b - s, I_{i-1}) || C#_{i-1} +/// O_i = SM4Encrypt(I_i, K) +/// C#_i = P#_i xor MSB(s, O_i) +/// ``` +/// +/// `encrypting` selects which of the two byte streams feeds the shift register: the ciphertext +/// segment, which is the output when encrypting and the input when decrypting. +fn reference_cfb8( + key: &KeyMaterial, + iv: [u8; BLOCK_LEN], + data: &mut [u8], + encrypting: bool, +) { + let sm4 = SM4Internal::new(key).expect("a valid SM4 key"); + let mut i = iv; + for byte in data.iter_mut() { + let mut o = i; + sm4.encrypt_block(&mut o); + let input = *byte; + // C#_i = P#_i xor MSB(8, O_i), one byte of the output block. + *byte ^= o[0]; + let feedback = if encrypting { *byte } else { input }; + // I_{i+1} = LSB(b - 8, I_i) || C#_i: drop the top byte, append the ciphertext byte. + i.rotate_left(1); + i[BLOCK_LEN - 1] = feedback; + } +} + +/// `SM4Key` agrees with the equations, both directions, at lengths either side of a block. +#[test] +fn the_cfb8_key_agrees_with_the_equations() { + let material = key_material(KEY_1); + let key = SM4Key::::from_key_material(&material).expect("a valid key"); + let iv = iv_bytes(); + + for len in [0usize, 1, 2, 15, 16, 17, 33, 64] { + let plaintext = filler(len, len as u32 + 7); + + let mut expected = plaintext.clone(); + reference_cfb8(&material, iv, &mut expected, true); + + let mut data = plaintext.clone(); + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encryption init"); + assert_eq!(got_iv, iv); + enc.do_encrypt_inplace(&mut data).expect("encryption"); + assert_eq!(data, expected, "len {len}: SM4Key encrypt should match the equations"); + + let mut back = data.clone(); + reference_cfb8(&material, iv, &mut back, false); + assert_eq!(back, plaintext, "len {len}: the reference decrypts its own output"); + + key.decrypt_inplace(&iv, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "len {len}: SM4Key decrypt should match"); + } +} diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index fb55fb094..bc1759f91 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -30,3 +30,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" + +[[bin]] +name = "bench_sm4_mem_usage" +path = "src/bench_sm4_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_sm4_mem_usage.rs b/mem_usage_benches/src/bench_sm4_mem_usage.rs new file mode 100644 index 000000000..ef09bd0cd --- /dev/null +++ b/mem_usage_benches/src/bench_sm4_mem_usage.rs @@ -0,0 +1,187 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sm4_mem_usage > /dev/null +//! +//! ms_print massif.out.* +//! ``` +//! +//! or, shoved all into one line: +//! +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sm4_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! Note: print!() is used to force the compiler not to optimize away the actual code. +//! The important stuff for benchmarking goes to stderr so the junk can be piped to /dev/null. +//! +//! Main is at the bottom, and controls which of these actually runs -- measure one at a time, +//! because massif reports the peak across the whole process. +//! +//! # What to expect, and why massif cannot see it +//! +//! Like AES, SM4 has no interesting stack profile: a call needs a few hundred bytes -- the +//! four-block working state (64 bytes), the eight `u16` S-box planes (16 bytes) and the circuit's +//! spills -- which is below what massif resolves, since the process's own start-up reaches about +//! 7.7 kB of stack before `main` runs. So the massif number is the floor, not a measurement. +//! +//! The numbers that do mean something come from the compiler's frame-layout remarks, as for +//! `bench_aes_mem_usage`: build this binary with +//! `RUSTFLAGS="-C remark=prologepilog -C remark=stack-frame-layout"`, with `main` calling every +//! bench so none is dead-stripped, and sum the deepest chain of frames below each `measure` +//! closure. The closure is not the whole cost: `encrypt_4blocks`, `tau` and `ortho` keep their +//! own frames under it (`objdump -d` shows the calls), so the chain is closure, `encrypt_4blocks` +//! or `new`, `tau`, `ortho`, each with its 8-byte return address. The engine is built in an +//! `#[inline(never)]` helper so its frame is a sibling of the operation's, and the operation +//! runs in a non-inlined, non-returning closure so nothing crosses back across the boundary. +//! The persistent cost, the 128 bytes of round keys, is what `print_struct_sizes` prints. There +//! are no lookup tables; a table-driven engine adds a 256-byte table on top. +//! +//! With rustc 1.97 nightly on x86-64 the frames are: closures 264 B for `new`, 88 B for the one- +//! and four-block entry points and 104 B for the two-block ones; `new` 232 B; `encrypt_4blocks` +//! and `decrypt_4blocks` 120 B; `tau` 152 B; `ortho` 24 B. The crate docs tabulate the chains, +//! 704 B for `new`, 416 B for the one- and four-block calls and 432 B for the two-block ones. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::sm4::LANES; +use bouncycastle::sm4::hazmat::SM4Internal; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of the engine, i.e. the persistent cost of holding the round keys. +fn print_struct_sizes() { + use core::mem::size_of; + + // draft-ribose-cfrg-sm4-10 Sec 7.3: 32 round keys of 32 bits, so 128 bytes. + println!("size_of: {}", size_of::()); +} + +/// Runs the operation in its own frame. Returns nothing, so no result crosses the boundary and +/// the closure's frame is exactly the operation's. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// The draft-ribose-cfrg-sm4-10 Appendix A.1.1 key, so the engine under measurement is a known +/// one. +const KEY: [u8; 16] = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, +]; + +/// Wraps the hard-coded key. `#[inline(never)]` so the wrapping is a sibling frame of whatever +/// uses the key, not part of it. +#[inline(never)] +fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey).unwrap() +} + +/// Expands the key into an engine, in its own frame, so the expansion's temporaries are popped +/// before an operation on the engine runs. +#[inline(never)] +fn load_sm4() -> SM4Internal { + SM4Internal::new(&key()).unwrap() +} + +// ---- key expansion: the expansion is the operation, so it runs inside `measure` ------------ + +fn bench_sm4_key_expansion() { + eprintln!("SM4Internal::new (key expansion)"); + let key = key(); + measure(|| { + let sm4 = SM4Internal::new(&key).unwrap(); + print!("{sm4:?}"); + }); +} + +// ---- the six entry points --------------------------------------------------------------------- +// +// Every entry point runs the same four-lane circuit: a single block fills all four lanes with +// copies, two blocks fill two, so the working state is the same size throughout. The blocks are +// stack arrays in the closure, never a heap buffer, so massif's --heap=no does not hide them. + +fn bench_sm4_encrypt_block() { + eprintln!("SM4Internal::encrypt_block"); + let sm4 = load_sm4(); + measure(|| { + let mut block = [0x11u8; 16]; + sm4.encrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_sm4_decrypt_block() { + eprintln!("SM4Internal::decrypt_block"); + let sm4 = load_sm4(); + measure(|| { + let mut block = [0x11u8; 16]; + sm4.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_sm4_encrypt_2blocks() { + eprintln!("SM4Internal::encrypt_2blocks"); + let sm4 = load_sm4(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + sm4.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_sm4_decrypt_2blocks() { + eprintln!("SM4Internal::decrypt_2blocks"); + let sm4 = load_sm4(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + sm4.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_sm4_encrypt_4blocks() { + eprintln!("SM4Internal::encrypt_4blocks"); + let sm4 = load_sm4(); + measure(|| { + let mut blocks: [[u8; 16]; LANES] = + [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + sm4.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_sm4_decrypt_4blocks() { + eprintln!("SM4Internal::decrypt_4blocks"); + let sm4 = load_sm4(); + measure(|| { + let mut blocks: [[u8; 16]; LANES] = + [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + sm4.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_sm4_key_expansion() + // bench_sm4_encrypt_block() + // bench_sm4_decrypt_block() + // bench_sm4_encrypt_2blocks() + // bench_sm4_decrypt_2blocks() + // bench_sm4_encrypt_4blocks() + // bench_sm4_decrypt_4blocks() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 8c3f55926..be979bead 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -4,3 +4,4 @@ mod bench_ccm_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha3_mem_usage; +mod bench_sm4_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index ae250f332..b2f18ddb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,3 +15,4 @@ pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; pub use bouncycastle_sm3 as sm3; +pub use bouncycastle_sm4 as sm4;